diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6a30c4e --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: LLVM +ColumnLimit: 0 +SpacesInParentheses: false +BreakBeforeBraces: Allman \ No newline at end of file diff --git a/debugdraw.c b/debugdraw.c index f85bf4f..f1007c5 100644 --- a/debugdraw.c +++ b/debugdraw.c @@ -30,11 +30,6 @@ typedef struct Command // thread local variables so debug drawing in server thread // doesn't fuck up main thread -#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) -#define THREADLOCAL __declspec(thread) -#else -#define THREADLOCAL __thread -#endif static THREADLOCAL Command commands[MAX_COMMANDS] = {0}; static THREADLOCAL int command_i = 0; diff --git a/gamestate.c b/gamestate.c index 9bbfaa3..3109d67 100644 --- a/gamestate.c +++ b/gamestate.c @@ -1,12 +1,12 @@ #include #define QUEUE_IMPL -#include "stdbool.h" #include "queue.h" +#include "stdbool.h" #include "types.h" #include "ipsettings.h" // debug/developer settings -#include // assert logging +#include // assert logging #include // memset // do not use any global variables to process gamestate @@ -18,747 +18,745 @@ enum { - PLAYERS = 1 << 0, - BOXES = 1 << 1, + PLAYERS = 1 << 0, + BOXES = 1 << 1, }; -void __assert(bool cond, const char* file, int line, const char* cond_string) +void __assert(bool cond, const char *file, int line, const char *cond_string) { - if (!cond) - { - fprintf(stderr, "%s:%d | Assertion %s failed\n", file, line, cond_string); - } + if (!cond) + { + fprintf(stderr, "%s:%d | Assertion %s failed\n", file, line, cond_string); + } } - static V2 cp_to_v2(cpVect v) { - return (V2) { .x = (float)v.x, .y = (float)v.y }; + return (V2){.x = (float)v.x, .y = (float)v.y}; } static cpVect v2_to_cp(V2 v) { - return cpv(v.x, v.y); + return cpv(v.x, v.y); } -bool was_entity_deleted(GameState* gs, EntityID id) +bool was_entity_deleted(GameState *gs, EntityID id) { - if (id.generation == 0) - return false; // generation 0 means null entity ID, not a deleted entity - Entity* the_entity = &gs->entities[id.index]; - return (!the_entity->exists || the_entity->generation != id.generation); + if (id.generation == 0) + return false; // generation 0 means null entity ID, not a deleted entity + Entity *the_entity = &gs->entities[id.index]; + return (!the_entity->exists || the_entity->generation != id.generation); } -Entity* get_entity_even_if_dead(GameState* gs, EntityID id) +Entity *get_entity_even_if_dead(GameState *gs, EntityID id) { - if (id.generation == 0) - { - return NULL; - } - assert(id.index < gs->cur_next_entity || gs->cur_next_entity == 0); - assert(id.index < gs->max_entities); - Entity* to_return = &gs->entities[id.index]; - // don't validate the generation either - return to_return; + if (id.generation == 0) + { + return NULL; + } + if (!(id.index < gs->cur_next_entity || gs->cur_next_entity == 0)) + return NULL; + if (!(id.index < gs->max_entities)) + return NULL; + Entity *to_return = &gs->entities[id.index]; + // don't validate the generation either + return to_return; } // may return null if it doesn't exist anymore -Entity* get_entity(GameState* gs, EntityID id) +Entity *get_entity(GameState *gs, EntityID id) { - Entity* to_return = get_entity_even_if_dead(gs, id); - if (was_entity_deleted(gs, id)) - return NULL; - return to_return; + Entity *to_return = get_entity_even_if_dead(gs, id); + if (was_entity_deleted(gs, id)) + return NULL; + return to_return; } -EntityID get_id(GameState* gs, Entity* e) +EntityID get_id(GameState *gs, Entity *e) { - if (e == NULL) - return (EntityID) { 0 }; + if (e == NULL) + return (EntityID){0}; - size_t index = (e - gs->entities); - assert(index >= 0); - assert(index < gs->cur_next_entity); + size_t index = (e - gs->entities); + assert(index >= 0); + assert(index < gs->cur_next_entity); - return (EntityID) { - .generation = e->generation, - .index = (unsigned int)index, - }; + return (EntityID){ + .generation = e->generation, + .index = (unsigned int)index, + }; } -static Entity* cp_shape_entity(cpShape* shape) +static Entity *cp_shape_entity(cpShape *shape) { - return (Entity*)cpShapeGetUserData(shape); + return (Entity *)cpShapeGetUserData(shape); } -static Entity* cp_body_entity(cpBody* body) +static Entity *cp_body_entity(cpBody *body) { - return (Entity*)cpBodyGetUserData(body); + return (Entity *)cpBodyGetUserData(body); } -static GameState* cp_space_gs(cpSpace* space) +static GameState *cp_space_gs(cpSpace *space) { - return (GameState*)cpSpaceGetUserData(space); + return (GameState *)cpSpaceGetUserData(space); } -int grid_num_boxes(GameState* gs, Entity* e) +int grid_num_boxes(GameState *gs, Entity *e) { - assert(e->is_grid); - int to_return = 0; + assert(e->is_grid); + int to_return = 0; - BOXES_ITER(gs, cur, e) - to_return++; + BOXES_ITER(gs, cur, e) + to_return++; - return to_return; + return to_return; } -void box_remove_from_boxes(GameState* gs, Entity* box) +void box_remove_from_boxes(GameState *gs, Entity *box) { - assert(box->is_box); - Entity* prev_box = get_entity(gs, box->prev_box); - Entity* next_box = get_entity(gs, box->next_box); - if (prev_box != NULL) - { - if (prev_box->is_box) - prev_box->next_box = get_id(gs, next_box); - else if (prev_box->is_grid) - prev_box->boxes = get_id(gs, next_box); - } - if (next_box != NULL) - { - assert(next_box->is_box); - next_box->prev_box = get_id(gs, prev_box); - } - box->next_box = (EntityID){ 0 }; - box->prev_box = (EntityID){ 0 }; + assert(box->is_box); + Entity *prev_box = get_entity(gs, box->prev_box); + Entity *next_box = get_entity(gs, box->next_box); + if (prev_box != NULL) + { + if (prev_box->is_box) + prev_box->next_box = get_id(gs, next_box); + else if (prev_box->is_grid) + prev_box->boxes = get_id(gs, next_box); + } + if (next_box != NULL) + { + assert(next_box->is_box); + next_box->prev_box = get_id(gs, prev_box); + } + box->next_box = (EntityID){0}; + box->prev_box = (EntityID){0}; } -void on_entity_child_shape(cpBody* body, cpShape* shape, void* data); +void on_entity_child_shape(cpBody *body, cpShape *shape, void *data); // gs is for iterating over all child shapes and destroying those, too -static void destroy_body(GameState* gs, cpBody** body) -{ - if (*body != NULL) - { - cpBodyEachShape(*body, on_entity_child_shape, (void*)gs); - cpSpaceRemoveBody(gs->space, *body); - cpBodyFree(*body); - *body = NULL; - } - *body = NULL; +static void destroy_body(GameState *gs, cpBody **body) +{ + if (*body != NULL) + { + cpBodyEachShape(*body, on_entity_child_shape, (void *)gs); + cpSpaceRemoveBody(gs->space, *body); + cpBodyFree(*body); + *body = NULL; + } + *body = NULL; } -void entity_destroy(GameState* gs, Entity* e) +void entity_destroy(GameState *gs, Entity *e) { - assert(e->exists); + assert(e->exists); - if (e->is_grid) - { - BOXES_ITER(gs, cur, e) - entity_destroy(gs, cur); - } - if (e->is_box) - { - box_remove_from_boxes(gs, e); - } + if (e->is_grid) + { + BOXES_ITER(gs, cur, e) + entity_destroy(gs, cur); + } + if (e->is_box) + { + box_remove_from_boxes(gs, e); + } - if (e->shape != NULL) - { - cpSpaceRemoveShape(gs->space, e->shape); - cpShapeFree(e->shape); - e->shape = NULL; - } - destroy_body(gs, &e->body); + if (e->shape != NULL) + { + cpSpaceRemoveShape(gs->space, e->shape); + cpShapeFree(e->shape); + e->shape = NULL; + } + destroy_body(gs, &e->body); - Entity* front_of_free_list = get_entity(gs, gs->free_list); - if (front_of_free_list != NULL) - assert(!front_of_free_list->exists); - int gen = e->generation; - *e = (Entity){ 0 }; - e->generation = gen; - e->next_free_entity = gs->free_list; - gs->free_list = get_id(gs, e); + Entity *front_of_free_list = get_entity(gs, gs->free_list); + if (front_of_free_list != NULL) + assert(!front_of_free_list->exists); + int gen = e->generation; + *e = (Entity){0}; + e->generation = gen; + e->next_free_entity = gs->free_list; + gs->free_list = get_id(gs, e); } -void on_entity_child_shape(cpBody* body, cpShape* shape, void* data) +void on_entity_child_shape(cpBody *body, cpShape *shape, void *data) { - entity_destroy((GameState*)data, cp_shape_entity(shape)); + entity_destroy((GameState *)data, cp_shape_entity(shape)); } -Entity* new_entity(GameState* gs) +Entity *new_entity(GameState *gs) { - Entity* to_return = NULL; - Entity* possible_free_list = get_entity_even_if_dead(gs, gs->free_list); - if (possible_free_list != NULL) - { - assert(possible_free_list->generation == gs->free_list.generation); - to_return = possible_free_list; - assert(!to_return->exists); - gs->free_list = to_return->next_free_entity; - } - else - { - assert(gs->cur_next_entity < gs->max_entities); // too many entities if fails - to_return = &gs->entities[gs->cur_next_entity]; - gs->cur_next_entity++; - } + Entity *to_return = NULL; + Entity *possible_free_list = get_entity_even_if_dead(gs, gs->free_list); + if (possible_free_list != NULL) + { + assert(possible_free_list->generation == gs->free_list.generation); + to_return = possible_free_list; + assert(!to_return->exists); + gs->free_list = to_return->next_free_entity; + } + else + { + assert(gs->cur_next_entity < gs->max_entities); // too many entities if fails + to_return = &gs->entities[gs->cur_next_entity]; + gs->cur_next_entity++; + } - to_return->generation++; - to_return->exists = true; - return to_return; + to_return->generation++; + to_return->exists = true; + return to_return; } -void create_body(GameState* gs, Entity* e) +void create_body(GameState *gs, Entity *e) { - assert(gs->space != NULL); + assert(gs->space != NULL); - if (e->body != NULL) - { - cpSpaceRemoveBody(gs->space, e->body); - cpBodyFree(e->body); - e->body = NULL; - } + if (e->body != NULL) + { + cpSpaceRemoveBody(gs->space, e->body); + cpBodyFree(e->body); + e->body = NULL; + } - cpBody* body = cpSpaceAddBody(gs->space, cpBodyNew(0.0, 0.0)); // zeros for mass/moment of inertia means automatically calculated from its collision shapes - e->body = body; - cpBodySetUserData(e->body, (void*)e); + cpBody *body = cpSpaceAddBody(gs->space, cpBodyNew(0.0, 0.0)); // zeros for mass/moment of inertia means automatically calculated from its collision shapes + e->body = body; + cpBodySetUserData(e->body, (void *)e); } -V2 player_vel(GameState* gs, Entity* player) +V2 player_vel(GameState *gs, Entity *player) { - assert(player->is_player); - Entity* potential_seat = get_entity(gs, player->currently_inside_of_box); - if (potential_seat != NULL) - { - return cp_to_v2(cpBodyGetVelocity(get_entity(gs, potential_seat->shape_parent_entity)->body)); - } - else - { - return cp_to_v2(cpBodyGetVelocity(player->body)); - } + assert(player->is_player); + Entity *potential_seat = get_entity(gs, player->currently_inside_of_box); + if (potential_seat != NULL) + { + return cp_to_v2(cpBodyGetVelocity(get_entity(gs, potential_seat->shape_parent_entity)->body)); + } + else + { + return cp_to_v2(cpBodyGetVelocity(player->body)); + } } -void grid_create(GameState* gs, Entity* e) +void grid_create(GameState *gs, Entity *e) { - e->is_grid = true; - create_body(gs, e); + e->is_grid = true; + create_body(gs, e); } -void entity_set_rotation(Entity* e, float rot) +void entity_set_rotation(Entity *e, float rot) { - assert(e->body != NULL); - cpBodySetAngle(e->body, rot); + assert(e->body != NULL); + cpBodySetAngle(e->body, rot); } -void entity_set_pos(Entity* e, V2 pos) +void entity_set_pos(Entity *e, V2 pos) { - assert(e->is_grid); - assert(e->body != NULL); - cpBodySetPosition(e->body, v2_to_cp(pos)); + assert(e->is_grid); + assert(e->body != NULL); + cpBodySetPosition(e->body, v2_to_cp(pos)); } // size is (1/2 the width, 1/2 the height) -void create_rectangle_shape(GameState* gs, Entity* e, Entity* parent, V2 pos, V2 size, float mass) -{ - if (e->shape != NULL) - { - cpSpaceRemoveShape(gs->space, e->shape); - cpShapeFree(e->shape); - e->shape = NULL; - } - - cpBB box = cpBBNew(-size.x + pos.x, -size.y + pos.y, size.x + pos.x, size.y + pos.y); - cpVect verts[4] = { - cpv(box.r, box.b), - cpv(box.r, box.t), - cpv(box.l, box.t), - cpv(box.l, box.b), - }; - - e->shape_size = size; - e->shape_parent_entity = get_id(gs, parent); - e->shape = (cpShape*)cpPolyShapeInitRaw(cpPolyShapeAlloc(), parent->body, 4, verts, 0.0f); // this cast is done in chipmunk, not sure why it works - cpShapeSetUserData(e->shape, (void*)e); - cpShapeSetMass(e->shape, mass); - cpSpaceAddShape(gs->space, e->shape); +void create_rectangle_shape(GameState *gs, Entity *e, Entity *parent, V2 pos, V2 size, float mass) +{ + if (e->shape != NULL) + { + cpSpaceRemoveShape(gs->space, e->shape); + cpShapeFree(e->shape); + e->shape = NULL; + } + + cpBB box = cpBBNew(-size.x + pos.x, -size.y + pos.y, size.x + pos.x, size.y + pos.y); + cpVect verts[4] = { + cpv(box.r, box.b), + cpv(box.r, box.t), + cpv(box.l, box.t), + cpv(box.l, box.b), + }; + + e->shape_size = size; + e->shape_parent_entity = get_id(gs, parent); + e->shape = (cpShape *)cpPolyShapeInitRaw(cpPolyShapeAlloc(), parent->body, 4, verts, 0.0f); // this cast is done in chipmunk, not sure why it works + cpShapeSetUserData(e->shape, (void *)e); + cpShapeSetMass(e->shape, mass); + cpSpaceAddShape(gs->space, e->shape); } #define PLAYER_SHAPE_FILTER cpShapeFilterNew(CP_NO_GROUP, PLAYERS, CP_ALL_CATEGORIES) -void create_player(GameState* gs, Entity* e) +void create_player(GameState *gs, Entity *e) { - e->is_player = true; - e->no_save_to_disk = true; - create_body(gs, e); - create_rectangle_shape(gs, e, e, (V2) { 0 }, V2scale(PLAYER_SIZE, 0.5f), PLAYER_MASS); - cpShapeSetFilter(e->shape, PLAYER_SHAPE_FILTER); + e->is_player = true; + e->no_save_to_disk = true; + create_body(gs, e); + create_rectangle_shape(gs, e, e, (V2){0}, V2scale(PLAYER_SIZE, 0.5f), PLAYER_MASS); + cpShapeSetFilter(e->shape, PLAYER_SHAPE_FILTER); } -void box_add_to_boxes(GameState* gs, Entity* grid, Entity* box_to_add) +void box_add_to_boxes(GameState *gs, Entity *grid, Entity *box_to_add) { - box_to_add->next_box = get_id(gs, get_entity(gs, grid->boxes)); - box_to_add->prev_box = get_id(gs, grid); - if (get_entity(gs, box_to_add->next_box) != NULL) - { - get_entity(gs, box_to_add->next_box)->prev_box = get_id(gs, box_to_add); - } - grid->boxes = get_id(gs, box_to_add); + box_to_add->next_box = get_id(gs, get_entity(gs, grid->boxes)); + box_to_add->prev_box = get_id(gs, grid); + if (get_entity(gs, box_to_add->next_box) != NULL) + { + get_entity(gs, box_to_add->next_box)->prev_box = get_id(gs, box_to_add); + } + grid->boxes = get_id(gs, box_to_add); } // box must be passed as a parameter as the box added to chipmunk uses this pointer in its // user data. pos is in local coordinates. Adds the box to the grid's chain of boxes -void box_create(GameState* gs, Entity* new_box, Entity* grid, V2 pos) +void box_create(GameState *gs, Entity *new_box, Entity *grid, V2 pos) { - new_box->is_box = true; - assert(gs->space != NULL); - assert(grid->is_grid); + new_box->is_box = true; + assert(gs->space != NULL); + assert(grid->is_grid); - float halfbox = BOX_SIZE / 2.0f; + float halfbox = BOX_SIZE / 2.0f; - create_rectangle_shape(gs, new_box, grid, pos, (V2) { halfbox, halfbox }, 1.0f); + create_rectangle_shape(gs, new_box, grid, pos, (V2){halfbox, halfbox}, 1.0f); - cpShapeSetFilter(new_box->shape, cpShapeFilterNew(CP_NO_GROUP, BOXES, CP_ALL_CATEGORIES)); + cpShapeSetFilter(new_box->shape, cpShapeFilterNew(CP_NO_GROUP, BOXES, CP_ALL_CATEGORIES)); - box_add_to_boxes(gs, grid, new_box); + box_add_to_boxes(gs, grid, new_box); } // removes boxes from grid, then ensures that the rule that grids must not have // holes in them is applied. -static void grid_correct_for_holes(GameState* gs, struct Entity* grid) +static void grid_correct_for_holes(GameState *gs, struct Entity *grid) { - int num_boxes = grid_num_boxes(gs, grid); - if (num_boxes == 0) - { - entity_destroy(gs, grid); - return; - } - if (num_boxes == 1) - return; + int num_boxes = grid_num_boxes(gs, grid); + if (num_boxes == 0) + { + entity_destroy(gs, grid); + return; + } + if (num_boxes == 1) + return; - // could be a gap between boxes in the grid, separate into multiple grids + // could be a gap between boxes in the grid, separate into multiple grids - // goal: create list of "real grids" from this grid that have boxes which are - // ONLY connected horizontally and vertically. whichever one of these "real grids" - // has the most blocks stays the current grid, so - // if a player is inhabiting this ship it stays that ship. - // The other "real grids" are allocated as new grids + // goal: create list of "real grids" from this grid that have boxes which are + // ONLY connected horizontally and vertically. whichever one of these "real grids" + // has the most blocks stays the current grid, so + // if a player is inhabiting this ship it stays that ship. + // The other "real grids" are allocated as new grids #define MAX_SEPARATE_GRIDS 8 - EntityID separate_grids[MAX_SEPARATE_GRIDS] = { 0 }; - int cur_separate_grid_index = 0; - int cur_separate_grid_size = 0; - int processed_boxes = 0; - - int biggest_separate_grid_index = 0; - int biggest_separate_grid_length = 0; - - // process all boxes into separate, but correctly connected, grids - while (processed_boxes < num_boxes) - { - // grab an unprocessed box, one not in separate_grids, to start the flood fill - Entity* unprocessed = get_entity(gs, grid->boxes); - assert(unprocessed != NULL); - assert(unprocessed->is_box); - box_remove_from_boxes(gs, unprocessed); // no longer in the boxes list of the grid - - // flood fill from this unprocessed box, adding each result to cur_separate_grid_index, - // removing each block from the grid - // https://en.wikipedia.org/wiki/Flood_fill - { - // queue stuff @Robust use factored datastructure - EntityID Q = get_id(gs, unprocessed); - Entity* N = NULL; - while (true) - { - assert(!was_entity_deleted(gs, Q)); - N = get_entity(gs, Q); - if (N == NULL) // must mean that the queue is empty - break; - Q = N->next_box; - if (true) // if node "inside", this is always true - { - N->next_box = separate_grids[cur_separate_grid_index]; - separate_grids[cur_separate_grid_index] = get_id(gs, N); - cur_separate_grid_size++; - processed_boxes++; - - V2 cur_local_pos = entity_shape_pos(N); - const V2 dirs[] = { - (V2) { - .x = -1.0f, .y = 0.0f -}, -(V2) { - .x = 1.0f, .y = 0.0f -}, -(V2) { - .x = 0.0f, .y = 1.0f -}, -(V2) { - .x = 0.0f, .y = -1.0f -}, - }; - int num_dirs = sizeof(dirs) / sizeof(*dirs); - - for (int ii = 0; ii < num_dirs; ii++) - { - V2 dir = dirs[ii]; - // @Robust @Speed faster method, not O(N^2), of getting the box - // in the direction currently needed - V2 wanted_local_pos = V2add(cur_local_pos, V2scale(dir, BOX_SIZE)); - EntityID box_in_direction = (EntityID){ 0 }; - BOXES_ITER(gs, cur, grid) - { - if (V2equal(entity_shape_pos(cur), wanted_local_pos, 0.01f)) - { - box_in_direction = get_id(gs, cur); - break; - } - } - - Entity* newbox = get_entity(gs, box_in_direction); - if (newbox != NULL) - { - box_remove_from_boxes(gs, newbox); - newbox->next_box = Q; - Q = box_in_direction; - } - } - } - } - } - - if (cur_separate_grid_size > biggest_separate_grid_length) - { - biggest_separate_grid_length = cur_separate_grid_size; - biggest_separate_grid_index = cur_separate_grid_index; - } - cur_separate_grid_index++; - assert(cur_separate_grid_index < MAX_SEPARATE_GRIDS); - cur_separate_grid_size = 0; - } - - // create new grids for all lists of boxes except for the biggest one. - // delete the boxes out of the current grid as I pull boxes into separate ones - // which are no longer connected - for (int sepgrid_i = 0; sepgrid_i < MAX_SEPARATE_GRIDS; sepgrid_i++) - { - EntityID cur_separate_grid = separate_grids[sepgrid_i]; - if (get_entity(gs, cur_separate_grid) == NULL) - continue; // this separate grid is empty - - Entity* new_grid; - if (sepgrid_i == biggest_separate_grid_index) - { - new_grid = grid; - } - else - { - new_grid = new_entity(gs); - grid_create(gs, new_grid); - cpBodySetPosition(new_grid->body, cpBodyGetPosition(grid->body)); - cpBodySetAngle(new_grid->body, cpBodyGetAngle(grid->body)); - } - - Entity* cur = get_entity(gs, cur_separate_grid); - while (cur != NULL) - { - Entity* next = get_entity(gs, cur->next_box); - box_create(gs, cur, new_grid, entity_shape_pos(cur)); // destroys next/prev fields on cur - cur = next; - } - - cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, v2_to_cp(grid_com(new_grid)))); - cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(grid)); - } -} - -static void grid_remove_box(GameState* gs, struct Entity* grid, struct Entity* box) -{ - assert(grid->is_grid); - assert(box->is_box); - entity_destroy(gs, box); - grid_correct_for_holes(gs, grid); -} - -static cpBool on_damage(cpArbiter* arb, cpSpace* space, cpDataPointer userData) -{ - cpShape* a, * b; - cpArbiterGetShapes(arb, &a, &b); - - Entity* entity_a, * entity_b; - entity_a = cp_shape_entity(a); - entity_b = cp_shape_entity(b); - - float damage = V2length(cp_to_v2(cpArbiterTotalImpulse(arb))) * COLLISION_DAMAGE_SCALING; - - if (entity_a->is_box && entity_a->box_type == BoxExplosive) - entity_a->damage += 2.0f * EXPLOSION_DAMAGE_THRESHOLD; - if (entity_b->is_box && entity_b->box_type == BoxExplosive) - entity_b->damage += 2.0f * EXPLOSION_DAMAGE_THRESHOLD; - - if (damage > 0.05f) - { - // Log("Collision with damage %f\n", damage); - entity_a->damage += damage; - entity_b->damage += damage; - } - - // b must be the key passed into the post step removed, the key is cast into its shape - // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, b, NULL); - // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, a, NULL); - - return true; // keep colliding -} - -void initialize(GameState* gs, void* entity_arena, size_t entity_arena_size) -{ - *gs = (GameState){ 0 }; - memset(entity_arena, 0, entity_arena_size); // SUPER critical. Random vals in the entity data causes big problem - gs->entities = (Entity*)entity_arena; - gs->max_entities = (unsigned int)(entity_arena_size / sizeof(Entity)); - gs->space = cpSpaceNew(); - cpSpaceSetUserData(gs->space, (cpDataPointer)gs); // needed in the handler - cpCollisionHandler* handler = cpSpaceAddCollisionHandler(gs->space, 0, 0); // @Robust limit collision type to just blocks that can be damaged - handler->postSolveFunc = on_damage; -} -void destroy(GameState* gs) -{ - // can't zero out gs data because the entity memory arena is reused - // on deserialization - for (size_t i = 0; i < gs->max_entities; i++) - { - if (gs->entities[i].exists) - entity_destroy(gs, &gs->entities[i]); - } - cpSpaceFree(gs->space); - gs->space = NULL; - - for (size_t i = 0; i < gs->cur_next_entity; i++) - { - if (gs->entities[i].exists) - gs->entities[i] = (Entity){ 0 }; - } - gs->cur_next_entity = 0; + EntityID separate_grids[MAX_SEPARATE_GRIDS] = {0}; + int cur_separate_grid_index = 0; + int cur_separate_grid_size = 0; + int processed_boxes = 0; + + int biggest_separate_grid_index = 0; + int biggest_separate_grid_length = 0; + + // process all boxes into separate, but correctly connected, grids + while (processed_boxes < num_boxes) + { + // grab an unprocessed box, one not in separate_grids, to start the flood fill + Entity *unprocessed = get_entity(gs, grid->boxes); + assert(unprocessed != NULL); + assert(unprocessed->is_box); + box_remove_from_boxes(gs, unprocessed); // no longer in the boxes list of the grid + + // flood fill from this unprocessed box, adding each result to cur_separate_grid_index, + // removing each block from the grid + // https://en.wikipedia.org/wiki/Flood_fill + { + // queue stuff @Robust use factored datastructure + EntityID Q = get_id(gs, unprocessed); + Entity *N = NULL; + while (true) + { + assert(!was_entity_deleted(gs, Q)); + N = get_entity(gs, Q); + if (N == NULL) // must mean that the queue is empty + break; + Q = N->next_box; + if (true) // if node "inside", this is always true + { + N->next_box = separate_grids[cur_separate_grid_index]; + separate_grids[cur_separate_grid_index] = get_id(gs, N); + cur_separate_grid_size++; + processed_boxes++; + + V2 cur_local_pos = entity_shape_pos(N); + const V2 dirs[] = { + (V2){ + .x = -1.0f, .y = 0.0f}, + (V2){ + .x = 1.0f, .y = 0.0f}, + (V2){ + .x = 0.0f, .y = 1.0f}, + (V2){ + .x = 0.0f, .y = -1.0f}, + }; + int num_dirs = sizeof(dirs) / sizeof(*dirs); + + for (int ii = 0; ii < num_dirs; ii++) + { + V2 dir = dirs[ii]; + // @Robust @Speed faster method, not O(N^2), of getting the box + // in the direction currently needed + V2 wanted_local_pos = V2add(cur_local_pos, V2scale(dir, BOX_SIZE)); + EntityID box_in_direction = (EntityID){0}; + BOXES_ITER(gs, cur, grid) + { + if (V2equal(entity_shape_pos(cur), wanted_local_pos, 0.01f)) + { + box_in_direction = get_id(gs, cur); + break; + } + } + + Entity *newbox = get_entity(gs, box_in_direction); + if (newbox != NULL) + { + box_remove_from_boxes(gs, newbox); + newbox->next_box = Q; + Q = box_in_direction; + } + } + } + } + } + + if (cur_separate_grid_size > biggest_separate_grid_length) + { + biggest_separate_grid_length = cur_separate_grid_size; + biggest_separate_grid_index = cur_separate_grid_index; + } + cur_separate_grid_index++; + assert(cur_separate_grid_index < MAX_SEPARATE_GRIDS); + cur_separate_grid_size = 0; + } + + // create new grids for all lists of boxes except for the biggest one. + // delete the boxes out of the current grid as I pull boxes into separate ones + // which are no longer connected + for (int sepgrid_i = 0; sepgrid_i < MAX_SEPARATE_GRIDS; sepgrid_i++) + { + EntityID cur_separate_grid = separate_grids[sepgrid_i]; + if (get_entity(gs, cur_separate_grid) == NULL) + continue; // this separate grid is empty + + Entity *new_grid; + if (sepgrid_i == biggest_separate_grid_index) + { + new_grid = grid; + } + else + { + new_grid = new_entity(gs); + grid_create(gs, new_grid); + cpBodySetPosition(new_grid->body, cpBodyGetPosition(grid->body)); + cpBodySetAngle(new_grid->body, cpBodyGetAngle(grid->body)); + } + + Entity *cur = get_entity(gs, cur_separate_grid); + while (cur != NULL) + { + Entity *next = get_entity(gs, cur->next_box); + box_create(gs, cur, new_grid, entity_shape_pos(cur)); // destroys next/prev fields on cur + cur = next; + } + + cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, v2_to_cp(grid_com(new_grid)))); + cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(grid)); + } +} + +static void grid_remove_box(GameState *gs, struct Entity *grid, struct Entity *box) +{ + assert(grid->is_grid); + assert(box->is_box); + entity_destroy(gs, box); + grid_correct_for_holes(gs, grid); +} + +static cpBool on_damage(cpArbiter *arb, cpSpace *space, cpDataPointer userData) +{ + cpShape *a, *b; + cpArbiterGetShapes(arb, &a, &b); + + Entity *entity_a, *entity_b; + entity_a = cp_shape_entity(a); + entity_b = cp_shape_entity(b); + + float damage = V2length(cp_to_v2(cpArbiterTotalImpulse(arb))) * COLLISION_DAMAGE_SCALING; + + if (entity_a->is_box && entity_a->box_type == BoxExplosive) + entity_a->damage += 2.0f * EXPLOSION_DAMAGE_THRESHOLD; + if (entity_b->is_box && entity_b->box_type == BoxExplosive) + entity_b->damage += 2.0f * EXPLOSION_DAMAGE_THRESHOLD; + + if (damage > 0.05f) + { + // Log("Collision with damage %f\n", damage); + entity_a->damage += damage; + entity_b->damage += damage; + } + + // b must be the key passed into the post step removed, the key is cast into its shape + // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, b, NULL); + // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, a, NULL); + + return true; // keep colliding +} + +void initialize(GameState *gs, void *entity_arena, size_t entity_arena_size) +{ + *gs = (GameState){0}; + memset(entity_arena, 0, entity_arena_size); // SUPER critical. Random vals in the entity data causes big problem + gs->entities = (Entity *)entity_arena; + gs->max_entities = (unsigned int)(entity_arena_size / sizeof(Entity)); + gs->space = cpSpaceNew(); + cpSpaceSetUserData(gs->space, (cpDataPointer)gs); // needed in the handler + cpCollisionHandler *handler = cpSpaceAddCollisionHandler(gs->space, 0, 0); // @Robust limit collision type to just blocks that can be damaged + handler->postSolveFunc = on_damage; +} +void destroy(GameState *gs) +{ + // can't zero out gs data because the entity memory arena is reused + // on deserialization + for (size_t i = 0; i < gs->max_entities; i++) + { + if (gs->entities[i].exists) + entity_destroy(gs, &gs->entities[i]); + } + cpSpaceFree(gs->space); + gs->space = NULL; + + for (size_t i = 0; i < gs->cur_next_entity; i++) + { + if (gs->entities[i].exists) + gs->entities[i] = (Entity){0}; + } + gs->cur_next_entity = 0; } // center of mass, not the literal position -V2 grid_com(Entity* grid) +V2 grid_com(Entity *grid) { - return cp_to_v2(cpBodyLocalToWorld(grid->body, cpBodyGetCenterOfGravity(grid->body))); + return cp_to_v2(cpBodyLocalToWorld(grid->body, cpBodyGetCenterOfGravity(grid->body))); } -V2 grid_vel(Entity* grid) +V2 grid_vel(Entity *grid) { - return cp_to_v2(cpBodyGetVelocity(grid->body)); + return cp_to_v2(cpBodyGetVelocity(grid->body)); } -V2 grid_world_to_local(Entity* grid, V2 world) +V2 grid_world_to_local(Entity *grid, V2 world) { - return cp_to_v2(cpBodyWorldToLocal(grid->body, v2_to_cp(world))); + return cp_to_v2(cpBodyWorldToLocal(grid->body, v2_to_cp(world))); } -V2 grid_local_to_world(Entity* grid, V2 local) +V2 grid_local_to_world(Entity *grid, V2 local) { - return cp_to_v2(cpBodyLocalToWorld(grid->body, v2_to_cp(local))); + assert(grid->is_grid); + return cp_to_v2(cpBodyLocalToWorld(grid->body, v2_to_cp(local))); } // returned snapped position is in world coordinates -V2 grid_snapped_box_pos(Entity* grid, V2 world) +V2 grid_snapped_box_pos(Entity *grid, V2 world) { - V2 local = grid_world_to_local(grid, world); - local.x /= BOX_SIZE; - local.y /= BOX_SIZE; - local.x = roundf(local.x); - local.y = roundf(local.y); - local.x *= BOX_SIZE; - local.y *= BOX_SIZE; + V2 local = grid_world_to_local(grid, world); + local.x /= BOX_SIZE; + local.y /= BOX_SIZE; + local.x = roundf(local.x); + local.y = roundf(local.y); + local.x *= BOX_SIZE; + local.y *= BOX_SIZE; - return cp_to_v2(cpBodyLocalToWorld(grid->body, v2_to_cp(local))); + return cp_to_v2(cpBodyLocalToWorld(grid->body, v2_to_cp(local))); } -float entity_rotation(Entity* grid) +float entity_rotation(Entity *grid) { - return (float)cpBodyGetAngle(grid->body); + return (float)cpBodyGetAngle(grid->body); } -float entity_angular_velocity(Entity* grid) +float entity_angular_velocity(Entity *grid) { - return (float)cpBodyGetAngularVelocity(grid->body); + return (float)cpBodyGetAngularVelocity(grid->body); } -Entity* box_grid(Entity* box) +Entity *box_grid(Entity *box) { - return (Entity*)cpBodyGetUserData(cpShapeGetBody(box->shape)); + return (Entity *)cpBodyGetUserData(cpShapeGetBody(box->shape)); } // in local space -V2 entity_shape_pos(Entity* box) +V2 entity_shape_pos(Entity *box) { - return cp_to_v2(cpShapeGetCenterOfGravity(box->shape)); + return cp_to_v2(cpShapeGetCenterOfGravity(box->shape)); } -float entity_shape_mass(Entity* box) +float entity_shape_mass(Entity *box) { - assert(box->shape != NULL); - return (float)cpShapeGetMass(box->shape); + assert(box->shape != NULL); + return (float)cpShapeGetMass(box->shape); } -float box_rotation(Entity* box) +float box_rotation(Entity *box) { - return (float)cpBodyGetAngle(cpShapeGetBody(box->shape)); + return (float)cpBodyGetAngle(cpShapeGetBody(box->shape)); } -V2 entity_pos(Entity* e) +V2 entity_pos(Entity *e) { - if (e->is_box) - { - return V2add(entity_pos(box_grid(e)), V2rotate(entity_shape_pos(e), entity_rotation(box_grid(e)))); - } - else if (e->is_explosion) - { - return e->explosion_pos; - } - else - { - assert(e->body != NULL); - return cp_to_v2(cpBodyGetPosition(e->body)); - } + if (e->is_box) + { + return V2add(entity_pos(box_grid(e)), V2rotate(entity_shape_pos(e), entity_rotation(box_grid(e)))); + } + else if (e->is_explosion) + { + return e->explosion_pos; + } + else + { + assert(e->body != NULL); + return cp_to_v2(cpBodyGetPosition(e->body)); + } } struct BodyData { - V2 pos; - V2 vel; - float rotation; - float angular_velocity; + V2 pos; + V2 vel; + float rotation; + float angular_velocity; }; -void populate(cpBody* body, struct BodyData* data) +void populate(cpBody *body, struct BodyData *data) { - data->pos = cp_to_v2(cpBodyGetPosition(body)); - data->vel = cp_to_v2(cpBodyGetVelocity(body)); - data->rotation = (float)cpBodyGetAngle(body); - data->angular_velocity = (float)cpBodyGetAngularVelocity(body); + data->pos = cp_to_v2(cpBodyGetPosition(body)); + data->vel = cp_to_v2(cpBodyGetVelocity(body)); + data->rotation = (float)cpBodyGetAngle(body); + data->angular_velocity = (float)cpBodyGetAngularVelocity(body); } -void update_from(cpBody* body, struct BodyData* data) +void update_from(cpBody *body, struct BodyData *data) { - cpBodySetPosition(body, v2_to_cp(data->pos)); - cpBodySetVelocity(body, v2_to_cp(data->vel)); - cpBodySetAngle(body, data->rotation); - cpBodySetAngularVelocity(body, data->angular_velocity); + cpBodySetPosition(body, v2_to_cp(data->pos)); + cpBodySetVelocity(body, v2_to_cp(data->vel)); + cpBodySetAngle(body, data->rotation); + cpBodySetAngularVelocity(body, data->angular_velocity); } typedef struct SerState { - char* bytes; - bool serializing; - size_t cursor; // points to next available byte, is the size of current message after serializing something - size_t max_size; - Entity* for_player; - size_t max_entity_index; // for error checking - bool write_varnames; - bool save_or_load_from_disk; - - // output - uint32_t version; + char *bytes; + bool serializing; + size_t cursor; // points to next available byte, is the size of current message after serializing something + size_t max_size; + Entity *for_player; + size_t max_entity_index; // for error checking + bool write_varnames; + bool save_or_load_from_disk; + + // output + uint32_t version; } SerState; typedef struct SerMaybeFailure { - bool failed; - int line; - const char* expression; + bool failed; + int line; + const char *expression; } SerMaybeFailure; -const static SerMaybeFailure ser_ok = { 0 }; -#define SER_ASSERT(cond) \ - if (!(cond)) \ - { \ - __assert(false, __FILE__, __LINE__, #cond); \ - if (ser->save_or_load_from_disk) \ - { \ - Log("While saving/loading, serialization assertion failed %s on line %d\n", #cond, __LINE__); \ - } \ - else \ - { \ - return (SerMaybeFailure){.failed = true, .line = __LINE__, .expression = #cond}; \ - } \ - } -#define SER_MAYBE_RETURN(maybe_failure) \ - { \ - SerMaybeFailure result = maybe_failure; \ - if (result.failed) \ - return result; \ - } -SerMaybeFailure ser_data(SerState* ser, char* data, size_t data_len, const char* name, const char* file, int line) -{ - char var_name[512] = { 0 }; - size_t var_name_len = 0; - if (ser->write_varnames) - { - snprintf(var_name, 512, "%d%s", line, name); // can't have separator before the name, when comparing names skips past the digit - var_name_len = strlen(var_name); - } - if (ser->serializing) - { - if (ser->write_varnames) - { - memcpy(ser->bytes + ser->cursor, var_name, var_name_len); - ser->cursor += var_name_len; - } - for (int b = 0; b < data_len; b++) - { - ser->bytes[ser->cursor] = data[b]; - ser->cursor += 1; - SER_ASSERT(ser->cursor < ser->max_size); - } - } - else - { - if (ser->write_varnames) - { - char read_name[512] = { 0 }; - - size_t just_field_name = strlen(name); - int i = 0; - int nondigit_i = 0; - while (true) - { - read_name[i] = ser->bytes[ser->cursor]; - if (nondigit_i == 0 && read_name[i] >= '0' && read_name[i] <= '9') - { - // still a digit - if (i >= 10) - { // 10 is way too many digits for a line number... - return (SerMaybeFailure) { - .expression = "Way too many digits as a line number before a field name", - .failed = true, - .line = __LINE__, - }; - } - } - else - { - nondigit_i += 1; - } - i++; - ser->cursor += 1; - SER_ASSERT(ser->cursor <= ser->max_size); - if (nondigit_i >= just_field_name) - break; - } - read_name[i + 1] = '\0'; - // advance past digits - char* read = read_name; - char* var = var_name; - while (*read >= '0' && *read <= '9') - read++; - while (*var >= '0' && *var <= '9') - var++; - SER_ASSERT(strcmp(read, var) == 0); - } - for (int b = 0; b < data_len; b++) - { - data[b] = ser->bytes[ser->cursor]; - ser->cursor += 1; - SER_ASSERT(ser->cursor <= ser->max_size); - } - } - return ser_ok; -} -SerMaybeFailure ser_var(SerState* ser, char* var_pointer, size_t var_size, const char* name, const char* file, int line) -{ - return ser_data(ser, var_pointer, var_size, name, file, line); +const static SerMaybeFailure ser_ok = {0}; +#define SER_ASSERT(cond) \ + if (!(cond)) \ + { \ + __assert(false, __FILE__, __LINE__, #cond); \ + if (ser->save_or_load_from_disk) \ + { \ + Log("While saving/loading, serialization assertion failed %s on line %d\n", #cond, __LINE__); \ + } \ + else \ + { \ + return (SerMaybeFailure){.failed = true, .line = __LINE__, .expression = #cond}; \ + } \ + } +#define SER_MAYBE_RETURN(maybe_failure) \ + { \ + SerMaybeFailure result = maybe_failure; \ + if (result.failed) \ + return result; \ + } +SerMaybeFailure ser_data(SerState *ser, char *data, size_t data_len, const char *name, const char *file, int line) +{ + char var_name[512] = {0}; + size_t var_name_len = 0; + if (ser->write_varnames) + { + snprintf(var_name, 512, "%d%s", line, name); // can't have separator before the name, when comparing names skips past the digit + var_name_len = strlen(var_name); + } + if (ser->serializing) + { + if (ser->write_varnames) + { + memcpy(ser->bytes + ser->cursor, var_name, var_name_len); + ser->cursor += var_name_len; + } + for (int b = 0; b < data_len; b++) + { + ser->bytes[ser->cursor] = data[b]; + ser->cursor += 1; + SER_ASSERT(ser->cursor < ser->max_size); + } + } + else + { + if (ser->write_varnames) + { + char read_name[512] = {0}; + + size_t just_field_name = strlen(name); + int i = 0; + int nondigit_i = 0; + while (true) + { + read_name[i] = ser->bytes[ser->cursor]; + if (nondigit_i == 0 && read_name[i] >= '0' && read_name[i] <= '9') + { + // still a digit + if (i >= 10) + { // 10 is way too many digits for a line number... + return (SerMaybeFailure){ + .expression = "Way too many digits as a line number before a field name", + .failed = true, + .line = __LINE__, + }; + } + } + else + { + nondigit_i += 1; + } + i++; + ser->cursor += 1; + SER_ASSERT(ser->cursor <= ser->max_size); + if (nondigit_i >= just_field_name) + break; + } + read_name[i + 1] = '\0'; + // advance past digits + char *read = read_name; + char *var = var_name; + while (*read >= '0' && *read <= '9') + read++; + while (*var >= '0' && *var <= '9') + var++; + SER_ASSERT(strcmp(read, var) == 0); + } + for (int b = 0; b < data_len; b++) + { + data[b] = ser->bytes[ser->cursor]; + ser->cursor += 1; + SER_ASSERT(ser->cursor <= ser->max_size); + } + } + return ser_ok; +} +SerMaybeFailure ser_var(SerState *ser, char *var_pointer, size_t var_size, const char *name, const char *file, int line) +{ + return ser_data(ser, var_pointer, var_size, name, file, line); } #define SER_DATA(data_pointer, data_length) SER_MAYBE_RETURN(ser_data(ser, data_pointer, data_length, #data_pointer, __FILE__, __LINE__)) #define SER_VAR_NAME(var_pointer, name) SER_MAYBE_RETURN(ser_var(ser, (char *)var_pointer, sizeof(*var_pointer), name, __FILE__, __LINE__)) @@ -766,1060 +764,1116 @@ SerMaybeFailure ser_var(SerState* ser, char* var_pointer, size_t var_size, const enum GameVersion { - VInitial, - VAddedTest, - VAddedSerToDisk, - VRemovedTest, - VChangedVectorSerializing, - VAddedLastUsedMedbay, - VAddedSquads, - VAddedSquadInvites, - VMax, // this minus one will be the version used + VInitial, + VAddedTest, + VAddedSerToDisk, + VRemovedTest, + VChangedVectorSerializing, + VAddedLastUsedMedbay, + VAddedSquads, + VAddedSquadInvites, + VRemovedTimeFromDiskSave, // did this to avoid wayy too big a time causing precision problems + VReallyRemovedTimeFromDiskSave, // apparently last one didn't work + VMax, // this minus one will be the version used }; // @Robust probably get rid of this as separate function, just use SER_VAR -SerMaybeFailure ser_V2(SerState* ser, V2* var) -{ - SER_VAR(&var->x); - SER_VAR(&var->y); - SER_ASSERT(!isnan(var->x)); - SER_ASSERT(!isnan(var->y)); - return ser_ok; -} - -SerMaybeFailure ser_bodydata(SerState* ser, struct BodyData* data) -{ - SER_MAYBE_RETURN(ser_V2(ser, &data->pos)); - SER_MAYBE_RETURN(ser_V2(ser, &data->vel)); - SER_VAR(&data->rotation); - SER_VAR(&data->angular_velocity); - SER_ASSERT(!isnan(data->rotation)); - SER_ASSERT(!isnan(data->angular_velocity)); - return ser_ok; -} - -SerMaybeFailure ser_entityid(SerState* ser, EntityID* id) -{ - SER_VAR(&id->generation); - SER_VAR(&id->index); - if (id->generation > 0) - SER_ASSERT(id->index < ser->max_entity_index); - return ser_ok; -} - -SerMaybeFailure ser_inputframe(SerState* ser, InputFrame* i) -{ - SER_VAR(&i->tick); - SER_VAR(&i->id); - SER_MAYBE_RETURN(ser_V2(ser, &i->movement)); - SER_VAR(&i->take_over_squad); - SER_ASSERT(i->take_over_squad >= 0 || i->take_over_squad == -1); - SER_ASSERT(i->take_over_squad < SquadLast); - if (ser->version >= VAddedSquadInvites) - { - SER_VAR(&i->accept_cur_squad_invite); - SER_VAR(&i->reject_cur_squad_invite); - SER_MAYBE_RETURN(ser_entityid(ser, &i->invite_this_player)); - } - - SER_VAR(&i->seat_action); - SER_MAYBE_RETURN(ser_entityid(ser, &i->seat_to_inhabit)); - SER_MAYBE_RETURN(ser_V2(ser, &i->hand_pos)); - SER_MAYBE_RETURN(ser_entityid(ser, &i->grid_hand_pos_local_to)); - - SER_VAR(&i->dobuild); - SER_VAR(&i->build_type); - SER_ASSERT(i->build_type >= 0); - SER_ASSERT(i->build_type < BoxLast); - SER_VAR(&i->build_rotation); - - return ser_ok; -} - -SerMaybeFailure ser_player(SerState* ser, Player* p) -{ - SER_VAR(&p->connected); - if (p->connected) - { - SER_VAR(&p->unlocked_bombs); - if (ser->version >= VAddedSquads) - SER_VAR(&p->squad); - SER_MAYBE_RETURN(ser_entityid(ser, &p->entity)); - if (ser->version >= VAddedLastUsedMedbay) - SER_MAYBE_RETURN(ser_entityid(ser, &p->last_used_medbay)); - SER_MAYBE_RETURN(ser_inputframe(ser, &p->input)); - } - - return ser_ok; -} - -SerMaybeFailure ser_entity(SerState* ser, GameState* gs, Entity* e) -{ - SER_VAR(&e->no_save_to_disk); // this is always false when saving to disk? - SER_VAR(&e->generation); - SER_VAR(&e->damage); - - int test; - if (ser->version < VRemovedTest && ser->version >= VAddedTest) - SER_VAR(&test); - - bool has_body = ser->serializing && e->body != NULL; - SER_VAR(&has_body); - - if (has_body) - { - struct BodyData body_data; - if (ser->serializing) - populate(e->body, &body_data); - SER_MAYBE_RETURN(ser_bodydata(ser, &body_data)); - if (!ser->serializing) - { - create_body(gs, e); - update_from(e->body, &body_data); - } - } - - bool has_shape = ser->serializing && e->shape != NULL; - SER_VAR(&has_shape); - - if (has_shape) - { - SER_MAYBE_RETURN(ser_V2(ser, &e->shape_size)); - SER_MAYBE_RETURN(ser_entityid(ser, &e->shape_parent_entity)); - Entity* parent = get_entity(gs, e->shape_parent_entity); - SER_ASSERT(parent != NULL); - - V2 shape_pos; - if (ser->serializing) - shape_pos = entity_shape_pos(e); - if (ser->version < VChangedVectorSerializing) - { - SER_VAR(&shape_pos); - } - else - { - SER_MAYBE_RETURN(ser_V2(ser, &shape_pos)); - } - - float shape_mass; - if (ser->serializing) - shape_mass = entity_shape_mass(e); - SER_VAR(&shape_mass); - SER_ASSERT(!isnan(shape_mass)); - - cpShapeFilter filter; - if (ser->serializing) - { - filter = cpShapeGetFilter(e->shape); - } - SER_VAR(&filter.categories); - SER_VAR(&filter.group); - SER_VAR(&filter.mask); - if (!ser->serializing) - { - create_rectangle_shape(gs, e, parent, shape_pos, e->shape_size, shape_mass); - cpShapeSetFilter(e->shape, filter); - } - } - - SER_VAR(&e->is_player); - if (e->is_player) - { - SER_ASSERT(e->no_save_to_disk); - - SER_MAYBE_RETURN(ser_entityid(ser, &e->currently_inside_of_box)); - if (ser->version >= VAddedSquads) - SER_VAR(&e->presenting_squad); - if (ser->version >= VAddedSquadInvites) - SER_VAR(&e->squad_invited_to); - SER_VAR(&e->goldness); - } - - SER_VAR(&e->is_explosion); - if (e->is_explosion) - { - SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_pos)); - SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_vel)); - SER_VAR(&e->explosion_progresss); - } - - SER_VAR(&e->is_grid); - if (e->is_grid) - { - SER_VAR(&e->total_energy_capacity); - SER_MAYBE_RETURN(ser_entityid(ser, &e->boxes)); - } - - SER_VAR(&e->is_box); - if (e->is_box) - { - SER_VAR(&e->box_type); - SER_VAR(&e->always_visible); - SER_VAR(&e->is_explosion_unlock); - SER_MAYBE_RETURN(ser_entityid(ser, &e->next_box)); - SER_MAYBE_RETURN(ser_entityid(ser, &e->prev_box)); - SER_VAR(&e->compass_rotation); - SER_VAR(&e->indestructible); - SER_VAR(&e->thrust); - SER_VAR(&e->wanted_thrust); - SER_VAR(&e->energy_used); - SER_VAR(&e->sun_amount); - SER_MAYBE_RETURN(ser_entityid(ser, &e->player_who_is_inside_of_me)); - } - - return ser_ok; -} - -SerMaybeFailure ser_opus_packets(SerState* ser, Queue* mic_or_speaker_data) -{ - bool no_more_packets = false; - if (ser->serializing) - { - size_t queued = queue_num_elements(mic_or_speaker_data); - for (size_t i = 0; i < queued; i++) - { - SER_VAR(&no_more_packets); - OpusPacket* cur = (OpusPacket*)queue_pop_element(mic_or_speaker_data); - bool isnull = cur == NULL; - SER_VAR(&isnull); - if (!isnull && cur != NULL) // cur != NULL is to suppress VS warning - { - SER_VAR(&cur->length); - SER_DATA(cur->data, cur->length); - } - } - no_more_packets = true; - SER_VAR(&no_more_packets); - } - else - { - while (true) - { - SER_VAR(&no_more_packets); - if (no_more_packets) - break; - OpusPacket* cur = (OpusPacket*)queue_push_element(mic_or_speaker_data); - OpusPacket dummy; - if (cur == NULL) - cur = &dummy; // throw away this packet - bool isnull = false; - SER_VAR(&isnull); - if (!isnull) - { - SER_VAR(&cur->length); - SER_ASSERT(cur->length < VOIP_PACKET_MAX_SIZE); - SER_ASSERT(cur->length >= 0); - SER_DATA(cur->data, cur->length); - } - } - } - return ser_ok; -} - -SerMaybeFailure ser_server_to_client(SerState* ser, ServerToClient* s) -{ - SER_VAR(&ser->version); - SER_ASSERT(ser->version >= 0); - SER_ASSERT(ser->version < VMax); - - if (!ser->save_or_load_from_disk) - SER_MAYBE_RETURN(ser_opus_packets(ser, s->playback_buffer)); - - GameState* gs = s->cur_gs; - - int cur_next_entity = 0; - if (ser->serializing) - cur_next_entity = gs->cur_next_entity; - SER_VAR(&cur_next_entity); - SER_ASSERT(cur_next_entity <= ser->max_entity_index); - - if (!ser->serializing) - { - // avoid a memset here very expensive. que rico! - destroy(gs); - initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities)); - gs->cur_next_entity = 0; // updated on deserialization - } - - SER_VAR(&s->your_player); - SER_VAR(&gs->time); - - SER_MAYBE_RETURN(ser_V2(ser, &gs->goldpos)); - - if (!ser->save_or_load_from_disk) - { - // @Robust save player data with their ID or something somehow. Like local backup of their account - for (size_t i = 0; i < MAX_PLAYERS; i++) - { - SER_MAYBE_RETURN(ser_player(ser, &gs->players[i])); - } - } - if (ser->serializing) - { - bool entities_done = false; - for (size_t i = 0; i < gs->cur_next_entity; i++) - { - Entity* e = &gs->entities[i]; -#define SER_ENTITY() \ - SER_VAR(&entities_done); \ - SER_VAR(&i); \ - SER_MAYBE_RETURN(ser_entity(ser, gs, e)) - if (e->exists && !(ser->save_or_load_from_disk && e->no_save_to_disk)) - { - if (!e->is_box && !e->is_grid) - { - SER_ENTITY(); - } - if (e->is_grid) - { - bool serialized_grid_yet = false; - // serialize boxes always after bodies, so that by the time the boxes - // are loaded in the parent body is loaded in and can be referenced. - BOXES_ITER(gs, cur, e) - { - bool this_box_in_range = (ser->for_player == NULL || (ser->for_player != NULL && V2distsqr(entity_pos(ser->for_player), entity_pos(cur)) < VISION_RADIUS * VISION_RADIUS)); - if (cur->always_visible) - this_box_in_range = true; - if (this_box_in_range) - { - if (!serialized_grid_yet) - { - serialized_grid_yet = true; - SER_ENTITY(); - } - - // serialize this box - EntityID cur_id = get_id(gs, cur); - SER_ASSERT(cur_id.index < gs->max_entities); - SER_VAR(&entities_done); - size_t the_index = (size_t)cur_id.index; // super critical. Type of &i is size_t. @Robust add debug info in serialization for what size the expected type is, maybe string nameof the type - SER_VAR_NAME(&the_index, "&i"); - SER_MAYBE_RETURN(ser_entity(ser, gs, cur)); - } - } - } - } +SerMaybeFailure ser_V2(SerState *ser, V2 *var) +{ + SER_VAR(&var->x); + SER_VAR(&var->y); + SER_ASSERT(!isnan(var->x)); + SER_ASSERT(!isnan(var->y)); + return ser_ok; +} + +SerMaybeFailure ser_bodydata(SerState *ser, struct BodyData *data) +{ + SER_MAYBE_RETURN(ser_V2(ser, &data->pos)); + SER_MAYBE_RETURN(ser_V2(ser, &data->vel)); + SER_VAR(&data->rotation); + SER_VAR(&data->angular_velocity); + SER_ASSERT(!isnan(data->rotation)); + SER_ASSERT(!isnan(data->angular_velocity)); + return ser_ok; +} + +SerMaybeFailure ser_entityid(SerState *ser, EntityID *id) +{ + SER_VAR(&id->generation); + SER_VAR(&id->index); + if (id->generation > 0) + SER_ASSERT(id->index < ser->max_entity_index); + return ser_ok; +} + +SerMaybeFailure ser_inputframe(SerState *ser, InputFrame *i) +{ + SER_VAR(&i->tick); + SER_MAYBE_RETURN(ser_V2(ser, &i->movement)); + SER_VAR(&i->take_over_squad); + SER_ASSERT(i->take_over_squad >= 0 || i->take_over_squad == -1); + SER_ASSERT(i->take_over_squad < SquadLast); + if (ser->version >= VAddedSquadInvites) + { + SER_VAR(&i->accept_cur_squad_invite); + SER_VAR(&i->reject_cur_squad_invite); + SER_MAYBE_RETURN(ser_entityid(ser, &i->invite_this_player)); + } + + SER_VAR(&i->seat_action); + SER_MAYBE_RETURN(ser_V2(ser, &i->hand_pos)); + + SER_VAR(&i->dobuild); + SER_VAR(&i->build_type); + SER_ASSERT(i->build_type >= 0); + SER_ASSERT(i->build_type < BoxLast); + SER_VAR(&i->build_rotation); + + return ser_ok; +} + +SerMaybeFailure ser_player(SerState *ser, Player *p) +{ + SER_VAR(&p->connected); + if (p->connected) + { + SER_VAR(&p->unlocked_bombs); + if (ser->version >= VAddedSquads) + SER_VAR(&p->squad); + SER_MAYBE_RETURN(ser_entityid(ser, &p->entity)); + if (ser->version >= VAddedLastUsedMedbay) + SER_MAYBE_RETURN(ser_entityid(ser, &p->last_used_medbay)); + SER_MAYBE_RETURN(ser_inputframe(ser, &p->input)); + } + + return ser_ok; +} + +SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e) +{ + SER_VAR(&e->no_save_to_disk); // @Robust this is always false when saving to disk? + SER_VAR(&e->generation); + SER_VAR(&e->damage); + + int test; + if (ser->version < VRemovedTest && ser->version >= VAddedTest) + SER_VAR(&test); + + bool has_body = ser->serializing && e->body != NULL; + SER_VAR(&has_body); + + if (has_body) + { + struct BodyData body_data; + if (ser->serializing) + populate(e->body, &body_data); + SER_MAYBE_RETURN(ser_bodydata(ser, &body_data)); + if (!ser->serializing) + { + create_body(gs, e); + update_from(e->body, &body_data); + } + } + + bool has_shape = ser->serializing && e->shape != NULL; + SER_VAR(&has_shape); + + if (has_shape) + { + SER_MAYBE_RETURN(ser_V2(ser, &e->shape_size)); + SER_MAYBE_RETURN(ser_entityid(ser, &e->shape_parent_entity)); + Entity *parent = get_entity(gs, e->shape_parent_entity); + SER_ASSERT(parent != NULL); + + V2 shape_pos; + if (ser->serializing) + shape_pos = entity_shape_pos(e); + if (ser->version < VChangedVectorSerializing) + { + SER_VAR(&shape_pos); + } + else + { + SER_MAYBE_RETURN(ser_V2(ser, &shape_pos)); + } + + float shape_mass; + if (ser->serializing) + shape_mass = entity_shape_mass(e); + SER_VAR(&shape_mass); + SER_ASSERT(!isnan(shape_mass)); + + cpShapeFilter filter; + if (ser->serializing) + { + filter = cpShapeGetFilter(e->shape); + } + SER_VAR(&filter.categories); + SER_VAR(&filter.group); + SER_VAR(&filter.mask); + if (!ser->serializing) + { + create_rectangle_shape(gs, e, parent, shape_pos, e->shape_size, shape_mass); + cpShapeSetFilter(e->shape, filter); + } + } + + SER_VAR(&e->is_player); + if (e->is_player) + { + SER_ASSERT(e->no_save_to_disk); + + SER_MAYBE_RETURN(ser_entityid(ser, &e->currently_inside_of_box)); + if (ser->version >= VAddedSquads) + SER_VAR(&e->presenting_squad); + if (ser->version >= VAddedSquadInvites) + SER_VAR(&e->squad_invited_to); + SER_VAR(&e->goldness); + } + + SER_VAR(&e->is_explosion); + if (e->is_explosion) + { + SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_pos)); + SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_vel)); + SER_VAR(&e->explosion_progresss); + } + + SER_VAR(&e->is_grid); + if (e->is_grid) + { + SER_VAR(&e->total_energy_capacity); + SER_MAYBE_RETURN(ser_entityid(ser, &e->boxes)); + } + + SER_VAR(&e->is_box); + if (e->is_box) + { + SER_VAR(&e->box_type); + SER_VAR(&e->always_visible); + SER_VAR(&e->is_explosion_unlock); + SER_MAYBE_RETURN(ser_entityid(ser, &e->next_box)); + SER_MAYBE_RETURN(ser_entityid(ser, &e->prev_box)); + SER_VAR(&e->compass_rotation); + SER_VAR(&e->indestructible); + SER_VAR(&e->thrust); + SER_VAR(&e->wanted_thrust); + SER_VAR(&e->energy_used); + SER_VAR(&e->sun_amount); + SER_MAYBE_RETURN(ser_entityid(ser, &e->player_who_is_inside_of_me)); + } + + return ser_ok; +} + +SerMaybeFailure ser_opus_packets(SerState *ser, Queue *mic_or_speaker_data) +{ + bool no_more_packets = false; + if (ser->serializing) + { + size_t queued = queue_num_elements(mic_or_speaker_data); + for (size_t i = 0; i < queued; i++) + { + SER_VAR(&no_more_packets); + OpusPacket *cur = (OpusPacket *)queue_pop_element(mic_or_speaker_data); + bool isnull = cur == NULL; + SER_VAR(&isnull); + if (!isnull && cur != NULL) // cur != NULL is to suppress VS warning + { + SER_VAR(&cur->length); + SER_DATA(cur->data, cur->length); + } + } + no_more_packets = true; + SER_VAR(&no_more_packets); + } + else + { + while (true) + { + SER_VAR(&no_more_packets); + if (no_more_packets) + break; + OpusPacket *cur = (OpusPacket *)queue_push_element(mic_or_speaker_data); + OpusPacket dummy; + if (cur == NULL) + cur = &dummy; // throw away this packet + bool isnull = false; + SER_VAR(&isnull); + if (!isnull) + { + SER_VAR(&cur->length); + SER_ASSERT(cur->length < VOIP_PACKET_MAX_SIZE); + SER_ASSERT(cur->length >= 0); + SER_DATA(cur->data, cur->length); + } + } + } + return ser_ok; +} + +SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s) +{ + SER_VAR(&ser->version); + SER_ASSERT(ser->version >= 0); + SER_ASSERT(ser->version < VMax); + + if (!ser->save_or_load_from_disk) + SER_MAYBE_RETURN(ser_opus_packets(ser, s->audio_playback_buffer)); + + GameState *gs = s->cur_gs; + + // completely reset and destroy all gamestate data + if (!ser->serializing) + { + // avoid a memset here very expensive. que rico! + destroy(gs); + initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities)); + gs->cur_next_entity = 0; // updated on deserialization + } + + int cur_next_entity = 0; + if (ser->serializing) + cur_next_entity = gs->cur_next_entity; + SER_VAR(&cur_next_entity); + SER_ASSERT(cur_next_entity <= ser->max_entity_index); + + if (!ser->save_or_load_from_disk) + SER_MAYBE_RETURN(ser_entityid(ser, &gs->cur_spacestation)); + + SER_VAR(&s->your_player); + if (ser->version >= VReallyRemovedTimeFromDiskSave && ser->save_or_load_from_disk) + { + } + else + { + SER_VAR(&gs->time); + } + + SER_MAYBE_RETURN(ser_V2(ser, &gs->goldpos)); + + if (!ser->save_or_load_from_disk) + { + // @Robust save player data with their ID or something somehow. Like local backup of their account + for (size_t i = 0; i < MAX_PLAYERS; i++) + { + SER_MAYBE_RETURN(ser_player(ser, &gs->players[i])); + } + } + if (ser->serializing) + { + bool entities_done = false; + for (size_t i = 0; i < gs->cur_next_entity; i++) + { + Entity *e = &gs->entities[i]; +#define SER_ENTITY() \ + SER_VAR(&entities_done); \ + SER_VAR(&i); \ + SER_MAYBE_RETURN(ser_entity(ser, gs, e)) + if (e->exists && !(ser->save_or_load_from_disk && e->no_save_to_disk)) + { + if (!e->is_box && !e->is_grid) + { + SER_ENTITY(); + } + if (e->is_grid) + { + bool serialized_grid_yet = false; + // serialize boxes always after bodies, so that by the time the boxes + // are loaded in the parent body is loaded in and can be referenced. + BOXES_ITER(gs, cur, e) + { + bool this_box_in_range = (ser->for_player == NULL || (ser->for_player != NULL && V2distsqr(entity_pos(ser->for_player), entity_pos(cur)) < VISION_RADIUS * VISION_RADIUS)); + if (cur->always_visible) + this_box_in_range = true; + if (this_box_in_range) + { + if (!serialized_grid_yet) + { + serialized_grid_yet = true; + SER_ENTITY(); + } + + // serialize this box + EntityID cur_id = get_id(gs, cur); + SER_ASSERT(cur_id.index < gs->max_entities); + SER_VAR(&entities_done); + size_t the_index = (size_t)cur_id.index; // super critical. Type of &i is size_t. @Robust add debug info in serialization for what size the expected type is, maybe string nameof the type + SER_VAR_NAME(&the_index, "&i"); + SER_MAYBE_RETURN(ser_entity(ser, gs, cur)); + } + } + } + } #undef SER_ENTITY - } - entities_done = true; - SER_VAR(&entities_done); - } - else - { - Entity* last_grid = NULL; - while (true) - { - bool entities_done = false; - SER_VAR(&entities_done); - if (entities_done) - break; - size_t next_index; - SER_VAR_NAME(&next_index, "&i"); - SER_ASSERT(next_index < gs->max_entities); - SER_ASSERT(next_index >= 0); - Entity* e = &gs->entities[next_index]; - e->exists = true; - // unsigned int possible_next_index = (unsigned int)(next_index + 2); // plus two because player entity refers to itself on deserialization - unsigned int possible_next_index = (unsigned int)(next_index + 1); - gs->cur_next_entity = gs->cur_next_entity < possible_next_index ? possible_next_index : gs->cur_next_entity; - SER_MAYBE_RETURN(ser_entity(ser, gs, e)); - - if (e->is_box) - { - SER_ASSERT(last_grid != NULL); - SER_ASSERT(get_entity(gs, e->shape_parent_entity) != NULL); - SER_ASSERT(last_grid == get_entity(gs, e->shape_parent_entity)); - e->prev_box = (EntityID){ 0 }; - e->next_box = (EntityID){ 0 }; - box_add_to_boxes(gs, last_grid, e); - } - - if (e->is_grid) - { - e->boxes = (EntityID){ 0 }; - last_grid = e; - } - } - for (size_t i = 0; i < gs->cur_next_entity; i++) - { - Entity* e = &gs->entities[i]; - if (!e->exists) - { - if (e->generation == 0) - e->generation = 1; // 0 generation reference is invalid, means null - e->next_free_entity = gs->free_list; - gs->free_list = get_id(gs, e); - } - } - } - return ser_ok; + } + entities_done = true; + SER_VAR(&entities_done); + } + else + { + Entity *last_grid = NULL; + while (true) + { + bool entities_done = false; + SER_VAR(&entities_done); + if (entities_done) + break; + size_t next_index; + SER_VAR_NAME(&next_index, "&i"); + SER_ASSERT(next_index < gs->max_entities); + SER_ASSERT(next_index >= 0); + Entity *e = &gs->entities[next_index]; + e->exists = true; + // unsigned int possible_next_index = (unsigned int)(next_index + 2); // plus two because player entity refers to itself on deserialization + unsigned int possible_next_index = (unsigned int)(next_index + 1); + gs->cur_next_entity = gs->cur_next_entity < possible_next_index ? possible_next_index : gs->cur_next_entity; + SER_MAYBE_RETURN(ser_entity(ser, gs, e)); + + if (e->is_box) + { + SER_ASSERT(last_grid != NULL); + SER_ASSERT(get_entity(gs, e->shape_parent_entity) != NULL); + SER_ASSERT(last_grid == get_entity(gs, e->shape_parent_entity)); + e->prev_box = (EntityID){0}; + e->next_box = (EntityID){0}; + box_add_to_boxes(gs, last_grid, e); + } + + if (e->is_grid) + { + e->boxes = (EntityID){0}; + last_grid = e; + } + } + for (size_t i = 0; i < gs->cur_next_entity; i++) + { + Entity *e = &gs->entities[i]; + if (!e->exists) + { + if (e->generation == 0) + e->generation = 1; // 0 generation reference is invalid, means null + e->next_free_entity = gs->free_list; + gs->free_list = get_id(gs, e); + } + } + } + return ser_ok; } // for_this_player can be null then the entire world will be sent -bool server_to_client_serialize(struct ServerToClient* msg, char* bytes, size_t* out_len, size_t max_len, Entity* for_this_player, bool to_disk) -{ - assert(msg->cur_gs != NULL); - assert(msg != NULL); - - SerState ser = (SerState){ - .bytes = bytes, - .serializing = true, - .cursor = 0, - .max_size = max_len, - .for_player = for_this_player, - .max_entity_index = msg->cur_gs->cur_next_entity, - .version = VMax - 1, - }; - - if (for_this_player == NULL) // @Robust jank - { - ser.save_or_load_from_disk = true; - } - - ser.write_varnames = to_disk; +bool server_to_client_serialize(struct ServerToClient *msg, char *bytes, size_t *out_len, size_t max_len, Entity *for_this_player, bool to_disk) +{ + assert(msg->cur_gs != NULL); + assert(msg != NULL); + + SerState ser = (SerState){ + .bytes = bytes, + .serializing = true, + .cursor = 0, + .max_size = max_len, + .for_player = for_this_player, + .max_entity_index = msg->cur_gs->cur_next_entity, + .version = VMax - 1, + }; + + if (for_this_player == NULL) // @Robust jank + { + ser.save_or_load_from_disk = true; + } + + ser.write_varnames = to_disk; #ifdef WRITE_VARNAMES - ser.write_varnames = true; + ser.write_varnames = true; #endif - SerMaybeFailure result = ser_server_to_client(&ser, msg); - *out_len = ser.cursor + 1; // @Robust not sure why I need to add one to cursor, ser.cursor should be the length.. - if (result.failed) - { - Log("Failed to serialize on line %d because of %s\n", result.line, result.expression); - return false; - } - else - { - return true; - } + SerMaybeFailure result = ser_server_to_client(&ser, msg); + *out_len = ser.cursor + 1; // @Robust not sure why I need to add one to cursor, ser.cursor should be the length.. + if (result.failed) + { + Log("Failed to serialize on line %d because of %s\n", result.line, result.expression); + return false; + } + else + { + return true; + } } -bool server_to_client_deserialize(struct ServerToClient* msg, char* bytes, size_t max_len, bool from_disk) +bool server_to_client_deserialize(struct ServerToClient *msg, char *bytes, size_t max_len, bool from_disk) { - assert(msg->cur_gs != NULL); - assert(msg != NULL); + assert(msg->cur_gs != NULL); + assert(msg != NULL); - SerState servar = (SerState){ - .bytes = bytes, - .serializing = false, - .cursor = 0, - .max_size = max_len, - .max_entity_index = msg->cur_gs->max_entities, - .save_or_load_from_disk = from_disk, - }; + SerState servar = (SerState){ + .bytes = bytes, + .serializing = false, + .cursor = 0, + .max_size = max_len, + .max_entity_index = msg->cur_gs->max_entities, + .save_or_load_from_disk = from_disk, + }; - if (from_disk) - servar.write_varnames = true; + if (from_disk) + servar.write_varnames = true; #ifdef WRITE_VARNAMES - servar.write_varnames = true; + servar.write_varnames = true; #endif - SerState* ser = &servar; - SerMaybeFailure result = ser_server_to_client(ser, msg); - if (result.failed) - { - Log("Failed to deserialize server to client on line %d because of %s\n", result.line, result.expression); - return false; - } - else - { - return true; - } -} - -SerMaybeFailure ser_client_to_server(SerState* ser, ClientToServer* msg) -{ - SER_VAR(&ser->version); - SER_MAYBE_RETURN(ser_opus_packets(ser, msg->mic_data)); - for (int i = 0; i < INPUT_BUFFER; i++) - { - SER_MAYBE_RETURN(ser_inputframe(ser, &msg->inputs[i])); - } - return ser_ok; -} - -bool client_to_server_serialize(GameState* gs, struct ClientToServer* msg, char* bytes, size_t* out_len, size_t max_len) -{ - SerState ser = (SerState){ - .bytes = bytes, - .serializing = true, - .cursor = 0, - .max_size = max_len, - .for_player = NULL, - .max_entity_index = gs->cur_next_entity, - .version = VMax - 1, - }; + SerState *ser = &servar; + SerMaybeFailure result = ser_server_to_client(ser, msg); + if (result.failed) + { + Log("Failed to deserialize server to client on line %d because of %s\n", result.line, result.expression); + return false; + } + else + { + return true; + } +} + +// only serializes up to the maximum inputs the server holds +SerMaybeFailure ser_client_to_server(SerState *ser, ClientToServer *msg) +{ + SER_VAR(&ser->version); + SER_MAYBE_RETURN(ser_opus_packets(ser, msg->mic_data)); + + // serialize input packets + size_t num; + if (ser->serializing) + { + num = queue_num_elements(msg->input_data); + if (num > INPUT_QUEUE_MAX) + num = INPUT_QUEUE_MAX; + } + SER_VAR(&num); + SER_ASSERT(num <= INPUT_QUEUE_MAX); + if (ser->serializing) + { + size_t to_skip = queue_num_elements(msg->input_data) - num; + size_t i = 0; + QUEUE_ITER(msg->input_data, cur_header) + { + if (i < to_skip) + { + i++; + } + else + { + InputFrame *cur = (InputFrame *)cur_header->data; + SER_MAYBE_RETURN(ser_inputframe(ser, cur)); + } + } + } + else + { + for (size_t i = 0; i < num; i++) + { + InputFrame *new_frame = (InputFrame *)queue_push_element(msg->input_data); + SER_ASSERT(new_frame != NULL); + SER_MAYBE_RETURN(ser_inputframe(ser, new_frame)); + } + } + return ser_ok; +} + +bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, char *bytes, size_t *out_len, size_t max_len) +{ + SerState ser = (SerState){ + .bytes = bytes, + .serializing = true, + .cursor = 0, + .max_size = max_len, + .for_player = NULL, + .max_entity_index = gs->cur_next_entity, + .version = VMax - 1, + }; #ifdef WRITE_VARNAMES - ser.write_varnames = true; + ser.write_varnames = true; #endif - SerMaybeFailure result = ser_client_to_server(&ser, msg); - *out_len = ser.cursor + 1; // see other comment for server to client - if (result.failed) - { - Log("Failed to serialize client to server because %s was false, line %d\n", result.expression, result.line); - return false; - } - else - { - return true; - } -} - -bool client_to_server_deserialize(GameState* gs, struct ClientToServer* msg, char* bytes, size_t max_len) -{ - SerState servar = (SerState){ - .bytes = bytes, - .serializing = false, - .cursor = 0, - .max_size = max_len, - .max_entity_index = gs->cur_next_entity, - .save_or_load_from_disk = false, - }; + SerMaybeFailure result = ser_client_to_server(&ser, msg); + *out_len = ser.cursor + 1; // see other comment for server to client + if (result.failed) + { + Log("Failed to serialize client to server because %s was false, line %d\n", result.expression, result.line); + return false; + } + else + { + return true; + } +} + +bool client_to_server_deserialize(GameState *gs, struct ClientToServer *msg, char *bytes, size_t max_len) +{ + SerState servar = (SerState){ + .bytes = bytes, + .serializing = false, + .cursor = 0, + .max_size = max_len, + .max_entity_index = gs->cur_next_entity, + .save_or_load_from_disk = false, + }; #ifdef WRITE_VARNAMES - servar.write_varnames = true; + servar.write_varnames = true; #endif - SerState* ser = &servar; - SerMaybeFailure result = ser_client_to_server(ser, msg); - if (result.failed) - { - Log("Failed to deserialize client to server on line %d because of %s\n", result.line, result.expression); - return false; - } - else - { - return true; - } + SerState *ser = &servar; + SerMaybeFailure result = ser_client_to_server(ser, msg); + if (result.failed) + { + Log("Failed to deserialize client to server on line %d because of %s\n", result.line, result.expression); + return false; + } + else + { + return true; + } } // has to be global var because can only get this information -static cpShape* closest_to_point_in_radius_result = NULL; -static float closest_to_point_in_radius_result_largest_dist = 0.0f; -static void closest_point_callback_func(cpShape* shape, cpContactPointSet* points, void* data) +static THREADLOCAL cpShape *closest_to_point_in_radius_result = NULL; +static THREADLOCAL float closest_to_point_in_radius_result_largest_dist = 0.0f; +static void closest_point_callback_func(cpShape *shape, cpContactPointSet *points, void *data) { - assert(points->count == 1); - if (!cp_shape_entity(shape)->is_box) - return; - float dist = V2length(cp_to_v2(cpvsub(points->points[0].pointA, points->points[0].pointB))); - // float dist = -points->points[0].distance; - if (dist > closest_to_point_in_radius_result_largest_dist) - { - closest_to_point_in_radius_result_largest_dist = dist; - closest_to_point_in_radius_result = shape; - } + assert(points->count == 1); + if (!cp_shape_entity(shape)->is_box) + return; + float dist = V2length(cp_to_v2(cpvsub(points->points[0].pointA, points->points[0].pointB))); + // float dist = -points->points[0].distance; + if (dist > closest_to_point_in_radius_result_largest_dist) + { + closest_to_point_in_radius_result_largest_dist = dist; + closest_to_point_in_radius_result = shape; + } } -Entity* closest_to_point_in_radius(GameState* gs, V2 point, float radius) +Entity *closest_to_point_in_radius(GameState *gs, V2 point, float radius) { - closest_to_point_in_radius_result = NULL; - closest_to_point_in_radius_result_largest_dist = 0.0f; + closest_to_point_in_radius_result = NULL; + closest_to_point_in_radius_result_largest_dist = 0.0f; - cpBody* tmpbody = cpBodyNew(0.0f, 0.0f); - cpShape* circle = cpCircleShapeNew(tmpbody, radius, v2_to_cp(point)); - cpSpaceShapeQuery(gs->space, circle, closest_point_callback_func, NULL); + cpBody *tmpbody = cpBodyNew(0.0f, 0.0f); + cpShape *circle = cpCircleShapeNew(tmpbody, radius, v2_to_cp(point)); + cpSpaceShapeQuery(gs->space, circle, closest_point_callback_func, NULL); - cpShapeFree(circle); - cpBodyFree(tmpbody); + cpShapeFree(circle); + cpBodyFree(tmpbody); - if (closest_to_point_in_radius_result != NULL) - { - // @Robust query here for only boxes that are part of ships, could get nasty... - return cp_body_entity(cpShapeGetBody(closest_to_point_in_radius_result)); - } + if (closest_to_point_in_radius_result != NULL) + { + // @Robust query here for only boxes that are part of ships, could get nasty... + return cp_body_entity(cpShapeGetBody(closest_to_point_in_radius_result)); + } - return NULL; + return NULL; } static float cur_explosion_damage = 0.0f; -static V2 explosion_origin = { 0 }; -static void explosion_callback_func(cpShape* shape, cpContactPointSet* points, void* data) +static V2 explosion_origin = {0}; +static void explosion_callback_func(cpShape *shape, cpContactPointSet *points, void *data) { - GameState* gs = (GameState*)data; - cp_shape_entity(shape)->damage += cur_explosion_damage; - Entity* parent = get_entity(gs, cp_shape_entity(shape)->shape_parent_entity); - V2 from_pos = entity_pos(cp_shape_entity(shape)); - V2 impulse = V2scale(V2normalize(V2sub(from_pos, explosion_origin)), EXPLOSION_PUSH_STRENGTH); - assert(parent->body != NULL); - cpBodyApplyImpulseAtWorldPoint(parent->body, v2_to_cp(impulse), v2_to_cp(from_pos)); + GameState *gs = (GameState *)data; + cp_shape_entity(shape)->damage += cur_explosion_damage; + Entity *parent = get_entity(gs, cp_shape_entity(shape)->shape_parent_entity); + V2 from_pos = entity_pos(cp_shape_entity(shape)); + V2 impulse = V2scale(V2normalize(V2sub(from_pos, explosion_origin)), EXPLOSION_PUSH_STRENGTH); + assert(parent->body != NULL); + cpBodyApplyImpulseAtWorldPoint(parent->body, v2_to_cp(impulse), v2_to_cp(from_pos)); } -static void do_explosion(GameState* gs, Entity* explosion, float dt) +static void do_explosion(GameState *gs, Entity *explosion, float dt) { - cur_explosion_damage = dt * EXPLOSION_DAMAGE_PER_SEC; - explosion_origin = explosion->explosion_pos; + cur_explosion_damage = dt * EXPLOSION_DAMAGE_PER_SEC; + explosion_origin = explosion->explosion_pos; - cpBody* tmpbody = cpBodyNew(0.0f, 0.0f); - cpShape* circle = cpCircleShapeNew(tmpbody, EXPLOSION_RADIUS, v2_to_cp(explosion_origin)); + cpBody *tmpbody = cpBodyNew(0.0f, 0.0f); + cpShape *circle = cpCircleShapeNew(tmpbody, EXPLOSION_RADIUS, v2_to_cp(explosion_origin)); - cpSpaceShapeQuery(gs->space, circle, explosion_callback_func, (void*)gs); + cpSpaceShapeQuery(gs->space, circle, explosion_callback_func, (void *)gs); - cpShapeFree(circle); - cpBodyFree(tmpbody); + cpShapeFree(circle); + cpBodyFree(tmpbody); } -V2 box_facing_vector(Entity* box) +V2 box_facing_vector(Entity *box) { - assert(box->is_box); - V2 to_return = (V2){ .x = 1.0f, .y = 0.0f }; + assert(box->is_box); + V2 to_return = (V2){.x = 1.0f, .y = 0.0f}; + + to_return = V2rotate(to_return, rotangle(box->compass_rotation)); + to_return = V2rotate(to_return, box_rotation(box)); - to_return = V2rotate(to_return, rotangle(box->compass_rotation)); - to_return = V2rotate(to_return, box_rotation(box)); + return to_return; +} - return to_return; +V2 thruster_force(Entity *box) +{ + return V2scale(box_facing_vector(box), -box->thrust * THRUSTER_FORCE); +} + +uint64_t tick(GameState *gs) +{ + return (uint64_t)floor(gs->time / ((double)TIMESTEP)); } -V2 thruster_force(Entity* box) +Entity *grid_to_build_on(GameState *gs, V2 world_hand_pos) { - return V2scale(box_facing_vector(box), -box->thrust * THRUSTER_FORCE); + return closest_to_point_in_radius(gs, world_hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP); } -uint64_t tick(GameState* gs) +V2 potentially_snap_hand_pos(GameState *gs, V2 world_hand_pos) { - return (uint64_t)floor(gs->time / ((double)TIMESTEP)); + Entity *potential_grid = grid_to_build_on(gs, world_hand_pos); + if (potential_grid != NULL) + { + world_hand_pos = grid_snapped_box_pos(potential_grid, world_hand_pos); + } + return world_hand_pos; } -V2 get_world_hand_pos(GameState* gs, InputFrame* input, Entity* player) +V2 get_world_hand_pos(GameState *gs, InputFrame *input, Entity *player) { - Entity* potential_grid = get_entity(gs, input->grid_hand_pos_local_to); - if (potential_grid != NULL) - { - return grid_local_to_world(potential_grid, input->hand_pos); - } - else - { - return V2add(entity_pos(player), input->hand_pos); - } + return potentially_snap_hand_pos(gs, V2add(entity_pos(player), input->hand_pos)); } // return true if used the energy -bool possibly_use_energy(GameState* gs, Entity* grid, float wanted_energy) +bool possibly_use_energy(GameState *gs, Entity *grid, float wanted_energy) { - BOXES_ITER(gs, possible_battery, grid) - { - if (possible_battery->box_type == BoxBattery && (BATTERY_CAPACITY - possible_battery->energy_used) > wanted_energy) - { - possible_battery->energy_used += wanted_energy; - return true; - } - } - return false; + BOXES_ITER(gs, possible_battery, grid) + { + if (possible_battery->box_type == BoxBattery && (BATTERY_CAPACITY - possible_battery->energy_used) > wanted_energy) + { + possible_battery->energy_used += wanted_energy; + return true; + } + } + return false; } -void entity_ensure_in_orbit(Entity* e) +void entity_ensure_in_orbit(Entity *e) { - cpVect pos = v2_to_cp(V2sub(entity_pos(e), SUN_POS)); - cpFloat r = cpvlength(pos); - cpFloat v = cpfsqrt(SUN_GRAVITY_STRENGTH / r) / r; - cpBodySetVelocity(e->body, cpvmult(cpvperp(pos), v)); + cpVect pos = v2_to_cp(V2sub(entity_pos(e), SUN_POS)); + cpFloat r = cpvlength(pos); + cpFloat v = cpfsqrt(SUN_GRAVITY_STRENGTH / r) / r; + cpBodySetVelocity(e->body, cpvmult(cpvperp(pos), v)); } -V2 box_vel(Entity* box) +V2 box_vel(Entity *box) { - assert(box->is_box); - Entity* grid = box_grid(box); - return cp_to_v2(cpBodyGetVelocityAtWorldPoint(grid->body, v2_to_cp(entity_pos(box)))); + assert(box->is_box); + Entity *grid = box_grid(box); + return cp_to_v2(cpBodyGetVelocityAtWorldPoint(grid->body, v2_to_cp(entity_pos(box)))); } -EntityID create_spacestation(GameState* gs) +EntityID create_spacestation(GameState *gs) { -#define BOX_AT_TYPE(grid, pos, type) \ - { \ - Entity *box = new_entity(gs); \ - box_create(gs, box, grid, pos); \ - box->box_type = type; \ - box->indestructible = indestructible; \ - box->always_visible = true; \ - box->no_save_to_disk = true; \ - } +#define BOX_AT_TYPE(grid, pos, type) \ + { \ + Entity *box = new_entity(gs); \ + box_create(gs, box, grid, pos); \ + box->box_type = type; \ + box->indestructible = indestructible; \ + box->always_visible = true; \ + box->no_save_to_disk = true; \ + } #define BOX_AT(grid, pos) BOX_AT_TYPE(grid, pos, BoxHullpiece) - bool indestructible = false; - Entity* grid = new_entity(gs); - grid_create(gs, grid); - grid->no_save_to_disk = true; - entity_set_pos(grid, (V2) { -150.0f, 0.0f }); - entity_ensure_in_orbit(grid); - Entity* explosion_box = new_entity(gs); - box_create(gs, explosion_box, grid, (V2) { 0 }); - explosion_box->is_explosion_unlock = true; - explosion_box->no_save_to_disk = true; - BOX_AT_TYPE(grid, ((V2){BOX_SIZE, 0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 2, 0}), BoxHullpiece); - BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 3, 0}), BoxHullpiece); - BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 4, 0}), BoxHullpiece); - - indestructible = true; - for (float y = -BOX_SIZE * 5.0; y <= BOX_SIZE * 5.0; y += BOX_SIZE) - { - BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 5.0, y}), BoxHullpiece); - } - for (float x = -BOX_SIZE * 5.0; x <= BOX_SIZE * 5.0; x += BOX_SIZE) - { - BOX_AT_TYPE(grid, ((V2){x, BOX_SIZE * 5.0}), BoxHullpiece); - BOX_AT_TYPE(grid, ((V2){x, -BOX_SIZE * 5.0}), BoxHullpiece); - } - indestructible = false; - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 5.0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 3.0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 1.0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 2.0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 3.0}), BoxExplosive); - BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 5.0}), BoxExplosive); - - return get_id(gs, grid); -} - -void exit_seat(GameState* gs, Entity* seat_in, Entity* player) -{ - V2 pilot_seat_exit_spot = V2add(entity_pos(seat_in), V2scale(box_facing_vector(seat_in), BOX_SIZE)); - cpBodySetPosition(player->body, v2_to_cp(pilot_seat_exit_spot)); - cpBodySetVelocity(player->body, v2_to_cp(player_vel(gs, player))); -} - -void process(GameState* gs, float dt) -{ - assert(gs->space != NULL); - - assert(dt == TIMESTEP); // @TODO fix tick being incremented every time - gs->time += dt; - - // process input - PLAYERS_ITER(gs->players, player) - { - if (player->input.take_over_squad >= 0) - { - if (player->input.take_over_squad == SquadNone) - { - player->squad = SquadNone; - } - else { - bool squad_taken = false; - PLAYERS_ITER(gs->players, other_player) - { - if (other_player->squad == player->input.take_over_squad) - { - squad_taken = true; - break; - } - } - if (!squad_taken) - player->squad = player->input.take_over_squad; - } - player->input.take_over_squad = -1; - } - - // squad invites - Entity* possibly_to_invite = get_entity(gs, player->input.invite_this_player); - if (player->input.invite_this_player.generation > 0) - player->input.invite_this_player = (EntityID){ 0 }; // just in case - if (player->squad != SquadNone && possibly_to_invite != NULL && possibly_to_invite->is_player) - { - possibly_to_invite->squad_invited_to = player->squad; - } - Entity* p = get_entity(gs, player->entity); - if (p == NULL) - { - p = new_entity(gs); - create_player(gs, p); - player->entity = get_id(gs, p); - Entity* medbay = get_entity(gs, player->last_used_medbay); - if (medbay != NULL) - { - exit_seat(gs, medbay, p); - } - entity_ensure_in_orbit(p); - } - assert(p->is_player); - p->presenting_squad = player->squad; - - if (p->squad_invited_to != SquadNone) - { - if (player->input.accept_cur_squad_invite) - { - player->squad = p->squad_invited_to; - p->squad_invited_to = SquadNone; - player->input.accept_cur_squad_invite = false; - } - if (player->input.reject_cur_squad_invite) - { - p->squad_invited_to = SquadNone; - player->input.reject_cur_squad_invite = false; - } - } + bool indestructible = false; + Entity *grid = new_entity(gs); + grid_create(gs, grid); + grid->no_save_to_disk = true; + entity_set_pos(grid, (V2){-15.0f, 0.0f}); + entity_ensure_in_orbit(grid); + Entity *explosion_box = new_entity(gs); + box_create(gs, explosion_box, grid, (V2){0}); + explosion_box->is_explosion_unlock = true; + explosion_box->no_save_to_disk = true; + BOX_AT_TYPE(grid, ((V2){BOX_SIZE, 0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 2, 0}), BoxHullpiece); + BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 3, 0}), BoxHullpiece); + BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 4, 0}), BoxHullpiece); + + indestructible = true; + for (float y = -BOX_SIZE * 5.0; y <= BOX_SIZE * 5.0; y += BOX_SIZE) + { + BOX_AT_TYPE(grid, ((V2){BOX_SIZE * 5.0, y}), BoxHullpiece); + } + for (float x = -BOX_SIZE * 5.0; x <= BOX_SIZE * 5.0; x += BOX_SIZE) + { + BOX_AT_TYPE(grid, ((V2){x, BOX_SIZE * 5.0}), BoxHullpiece); + BOX_AT_TYPE(grid, ((V2){x, -BOX_SIZE * 5.0}), BoxHullpiece); + } + indestructible = false; + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 5.0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 3.0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, BOX_SIZE * 1.0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 2.0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 3.0}), BoxExplosive); + BOX_AT_TYPE(grid, ((V2){-BOX_SIZE * 6.0, -BOX_SIZE * 5.0}), BoxExplosive); + + return get_id(gs, grid); +} + +void exit_seat(GameState *gs, Entity *seat_in, Entity *player) +{ + V2 pilot_seat_exit_spot = V2add(entity_pos(seat_in), V2scale(box_facing_vector(seat_in), BOX_SIZE)); + cpBodySetPosition(player->body, v2_to_cp(pilot_seat_exit_spot)); + cpBodySetVelocity(player->body, v2_to_cp(player_vel(gs, player))); +} + +void process_fixed_timestep(GameState *gs) +{ + process(gs, TIMESTEP); +} + +void process(GameState *gs, float dt) +{ + assert(gs->space != NULL); + + gs->time += dt; + + // process input + PLAYERS_ITER(gs->players, player) + { + if (player->input.take_over_squad >= 0) + { + if (player->input.take_over_squad == SquadNone) + { + player->squad = SquadNone; + } + else + { + bool squad_taken = false; + PLAYERS_ITER(gs->players, other_player) + { + if (other_player->squad == player->input.take_over_squad) + { + squad_taken = true; + break; + } + } + if (!squad_taken) + player->squad = player->input.take_over_squad; + } + player->input.take_over_squad = -1; + } + + // squad invites + Entity *possibly_to_invite = get_entity(gs, player->input.invite_this_player); + if (player->input.invite_this_player.generation > 0) + player->input.invite_this_player = (EntityID){0}; // just in case + if (player->squad != SquadNone && possibly_to_invite != NULL && possibly_to_invite->is_player) + { + possibly_to_invite->squad_invited_to = player->squad; + } + Entity *p = get_entity(gs, player->entity); + if (p == NULL) + { + p = new_entity(gs); + create_player(gs, p); + player->entity = get_id(gs, p); + Entity *medbay = get_entity(gs, player->last_used_medbay); + if (medbay != NULL) + { + exit_seat(gs, medbay, p); + } + entity_ensure_in_orbit(p); + } + assert(p->is_player); + p->presenting_squad = player->squad; + + if (p->squad_invited_to != SquadNone) + { + if (player->input.accept_cur_squad_invite) + { + player->squad = p->squad_invited_to; + p->squad_invited_to = SquadNone; + player->input.accept_cur_squad_invite = false; + } + if (player->input.reject_cur_squad_invite) + { + p->squad_invited_to = SquadNone; + player->input.reject_cur_squad_invite = false; + } + } #ifdef INFINITE_RESOURCES - p->damage = 0.0f; + p->damage = 0.0f; #endif - // update gold win condition - if (V2length(V2sub(cp_to_v2(cpBodyGetPosition(p->body)), gs->goldpos)) < GOLD_COLLECT_RADIUS) - { - p->goldness += 0.1f; - p->damage = 0.0f; - gs->goldpos = (V2){ .x = hash11((float)gs->time) * 20.0f, .y = hash11((float)gs->time - 13.6f) * 20.0f }; - } + // update gold win condition + if (V2length(V2sub(cp_to_v2(cpBodyGetPosition(p->body)), gs->goldpos)) < GOLD_COLLECT_RADIUS) + { + p->goldness += 0.1f; + p->damage = 0.0f; + gs->goldpos = (V2){.x = hash11((float)gs->time) * 20.0f, .y = hash11((float)gs->time - 13.6f) * 20.0f}; + } #if 1 - V2 world_hand_pos = get_world_hand_pos(gs, &player->input, p); - if (player->input.seat_action) - { - player->input.seat_action = false; // "handle" the input - Entity* seat_maybe_in = get_entity(gs, p->currently_inside_of_box); - if (seat_maybe_in == NULL) // not in any seat - { - cpPointQueryInfo query_info = { 0 }; - cpShape* result = cpSpacePointQueryNearest(gs->space, v2_to_cp(world_hand_pos), 0.1f, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &query_info); - if (result != NULL) - { - Entity* potential_seat = cp_shape_entity(result); - assert(potential_seat->is_box); - if (potential_seat->box_type == BoxCockpit || potential_seat->box_type == BoxMedbay) // @Robust check by feature flag instead of box type - { - // don't let players get inside of cockpits that somebody else is already inside of - if (get_entity(gs, potential_seat->player_who_is_inside_of_me) == NULL) - { - p->currently_inside_of_box = get_id(gs, potential_seat); - potential_seat->player_who_is_inside_of_me = get_id(gs, p); - player->last_used_medbay = p->currently_inside_of_box; - } - } - } - else - { - Log("No seat to get into for a player at point %f %f\n", world_hand_pos.x, world_hand_pos.y); - } - } - else - { - exit_seat(gs, seat_maybe_in, p); - seat_maybe_in->player_who_is_inside_of_me = (EntityID){ 0 }; - p->currently_inside_of_box = (EntityID){ 0 }; - } - } + V2 world_hand_pos = get_world_hand_pos(gs, &player->input, p); + if (player->input.seat_action) + { + player->input.seat_action = false; // "handle" the input + Entity *seat_maybe_in = get_entity(gs, p->currently_inside_of_box); + if (seat_maybe_in == NULL) // not in any seat + { + cpPointQueryInfo query_info = {0}; + cpShape *result = cpSpacePointQueryNearest(gs->space, v2_to_cp(world_hand_pos), 0.1f, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &query_info); + if (result != NULL) + { + Entity *potential_seat = cp_shape_entity(result); + assert(potential_seat->is_box); + if (potential_seat->box_type == BoxCockpit || potential_seat->box_type == BoxMedbay) // @Robust check by feature flag instead of box type + { + // don't let players get inside of cockpits that somebody else is already inside of + if (get_entity(gs, potential_seat->player_who_is_inside_of_me) == NULL) + { + p->currently_inside_of_box = get_id(gs, potential_seat); + potential_seat->player_who_is_inside_of_me = get_id(gs, p); + player->last_used_medbay = p->currently_inside_of_box; + } + } + } + else + { + Log("No seat to get into for a player at point %f %f\n", world_hand_pos.x, world_hand_pos.y); + } + } + else + { + exit_seat(gs, seat_maybe_in, p); + seat_maybe_in->player_who_is_inside_of_me = (EntityID){0}; + p->currently_inside_of_box = (EntityID){0}; + } + } #endif - // process movement - { - // no cheating by making movement bigger than length 1 - float movement_strength = V2length(player->input.movement); - if (movement_strength != 0.0f) - { - player->input.movement = V2scale(V2normalize(player->input.movement), clamp(V2length(player->input.movement), 0.0f, 1.0f)); - } - Entity* seat_inside_of = get_entity(gs, p->currently_inside_of_box); - - if (seat_inside_of == NULL) - { - cpShapeSetFilter(p->shape, PLAYER_SHAPE_FILTER); - cpBodyApplyForceAtWorldPoint(p->body, v2_to_cp(V2scale(player->input.movement, PLAYER_JETPACK_FORCE)), cpBodyGetPosition(p->body)); - p->damage += movement_strength * dt * PLAYER_JETPACK_SPICE_PER_SECOND; - } - else - { - assert(seat_inside_of->is_box); - cpShapeSetFilter(p->shape, CP_SHAPE_FILTER_NONE); // no collisions while in a seat - cpBodySetPosition(p->body, v2_to_cp(entity_pos(seat_inside_of))); - cpBodySetVelocity(p->body, v2_to_cp(box_vel(seat_inside_of))); - - // set thruster thrust from movement - if (seat_inside_of->box_type == BoxCockpit) - { - Entity* g = get_entity(gs, seat_inside_of->shape_parent_entity); - - V2 target_direction = { 0 }; - if (V2length(player->input.movement) > 0.0f) - { - target_direction = V2normalize(player->input.movement); - } - BOXES_ITER(gs, cur, g) - { - if (cur->box_type != BoxThruster) - continue; - float wanted_thrust = -V2dot(target_direction, box_facing_vector(cur)); - wanted_thrust = clamp01(wanted_thrust); - cur->wanted_thrust = wanted_thrust; - } - } - } - } + // process movement + { + // no cheating by making movement bigger than length 1 + V2 movement_this_tick = (V2){0}; + if (V2length(player->input.movement) > 0.0f) + { + movement_this_tick = V2scale(V2normalize(player->input.movement), clamp(V2length(player->input.movement), 0.0f, 1.0f)); + player->input.movement = (V2){0}; + } + Entity *seat_inside_of = get_entity(gs, p->currently_inside_of_box); + + if (seat_inside_of == NULL) + { + cpShapeSetFilter(p->shape, PLAYER_SHAPE_FILTER); + cpBodyApplyForceAtWorldPoint(p->body, v2_to_cp(V2scale(movement_this_tick, PLAYER_JETPACK_FORCE)), cpBodyGetPosition(p->body)); + p->damage += V2length(movement_this_tick) * dt * PLAYER_JETPACK_SPICE_PER_SECOND; + } + else + { + assert(seat_inside_of->is_box); + cpShapeSetFilter(p->shape, CP_SHAPE_FILTER_NONE); // no collisions while in a seat + cpBodySetPosition(p->body, v2_to_cp(entity_pos(seat_inside_of))); + cpBodySetVelocity(p->body, v2_to_cp(box_vel(seat_inside_of))); + + // set thruster thrust from movement + if (seat_inside_of->box_type == BoxCockpit) + { + Entity *g = get_entity(gs, seat_inside_of->shape_parent_entity); + + V2 target_direction = {0}; + if (V2length(movement_this_tick) > 0.0f) + { + target_direction = V2normalize(movement_this_tick); + } + BOXES_ITER(gs, cur, g) + { + if (cur->box_type != BoxThruster) + continue; + float wanted_thrust = -V2dot(target_direction, box_facing_vector(cur)); + wanted_thrust = clamp01(wanted_thrust); + cur->wanted_thrust = wanted_thrust; + } + } + } + } #if 1 // building - if (player->input.dobuild) - { - player->input.dobuild = false; // handle the input. if didn't do this, after destruction of hovered box, would try to build on its grid with grid_index... - - cpPointQueryInfo info = { 0 }; - V2 world_build = world_hand_pos; - - // @Robust sanitize this input so player can't build on any grid in the world - Entity* target_grid = get_entity(gs, player->input.grid_hand_pos_local_to); - cpShape* nearest = cpSpacePointQueryNearest(gs->space, v2_to_cp(world_build), 0.01f, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &info); - if (nearest != NULL) - { - Entity* cur_box = cp_shape_entity(nearest); - if (!cur_box->indestructible) - { - Entity* cur_grid = cp_body_entity(cpShapeGetBody(nearest)); - p->damage -= DAMAGE_TO_PLAYER_PER_BLOCK * ((BATTERY_CAPACITY - cur_box->energy_used) / BATTERY_CAPACITY); - grid_remove_box(gs, cur_grid, cur_box); - } - } - else if (target_grid == NULL) - { - Entity* new_grid = new_entity(gs); - grid_create(gs, new_grid); - p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; - entity_set_pos(new_grid, world_build); - - Entity* new_box = new_entity(gs); - box_create(gs, new_box, new_grid, (V2) { 0 }); - new_box->box_type = player->input.build_type; - new_box->compass_rotation = player->input.build_rotation; - cpBodySetVelocity(new_grid->body, v2_to_cp(player_vel(gs, p))); - } - else - { - Entity* new_box = new_entity(gs); - box_create(gs, new_box, target_grid, grid_world_to_local(target_grid, world_build)); - grid_correct_for_holes(gs, target_grid); // no holey ship for you! - new_box->box_type = player->input.build_type; - new_box->compass_rotation = player->input.build_rotation; - p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; - } - } + if (player->input.dobuild) + { + player->input.dobuild = false; // handle the input. if didn't do this, after destruction of hovered box, would try to build on its grid with grid_index... + + cpPointQueryInfo info = {0}; + V2 world_build = world_hand_pos; + + // @Robust sanitize this input so player can't build on any grid in the world + Entity *target_grid = grid_to_build_on(gs, world_hand_pos); + cpShape *nearest = cpSpacePointQueryNearest(gs->space, v2_to_cp(world_build), 0.01f, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &info); + if (nearest != NULL) + { + Entity *cur_box = cp_shape_entity(nearest); + if (!cur_box->indestructible) + { + Entity *cur_grid = cp_body_entity(cpShapeGetBody(nearest)); + p->damage -= DAMAGE_TO_PLAYER_PER_BLOCK * ((BATTERY_CAPACITY - cur_box->energy_used) / BATTERY_CAPACITY); + grid_remove_box(gs, cur_grid, cur_box); + } + } + else if (target_grid == NULL) + { + Entity *new_grid = new_entity(gs); + grid_create(gs, new_grid); + p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; + entity_set_pos(new_grid, world_build); + + Entity *new_box = new_entity(gs); + box_create(gs, new_box, new_grid, (V2){0}); + new_box->box_type = player->input.build_type; + new_box->compass_rotation = player->input.build_rotation; + cpBodySetVelocity(new_grid->body, v2_to_cp(player_vel(gs, p))); + } + else + { + Entity *new_box = new_entity(gs); + box_create(gs, new_box, target_grid, grid_world_to_local(target_grid, world_build)); + grid_correct_for_holes(gs, target_grid); // no holey ship for you! + new_box->box_type = player->input.build_type; + new_box->compass_rotation = player->input.build_rotation; + p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; + } + } #endif - if (p->damage >= 1.0f) - { - entity_destroy(gs, p); - player->entity = (EntityID){ 0 }; - } - - p->damage = clamp01(p->damage); -} - - if (get_entity(gs, gs->cur_spacestation) == NULL) - { - gs->cur_spacestation = create_spacestation(gs); - } - - // process entities - for (size_t i = 0; i < gs->cur_next_entity; i++) - { - Entity* e = &gs->entities[i]; - if (!e->exists) - continue; - - if (e->is_explosion_unlock) - { - PLAYERS_ITER(gs->players, player) - { - Entity* player_entity = get_entity(gs, player->entity); - if (player_entity != NULL && V2length(V2sub(entity_pos(player_entity), entity_pos(e))) < GOLD_UNLOCK_RADIUS) - { - player->unlocked_bombs = true; - } - } - } - - if (e->body != NULL) - { - cpVect p = cpvsub(cpBodyGetPosition(e->body), v2_to_cp(SUN_POS)); - cpFloat sqdist = cpvlengthsq(p); - if (sqdist > (INSTANT_DEATH_DISTANCE_FROM_SUN * INSTANT_DEATH_DISTANCE_FROM_SUN)) - { - entity_destroy(gs, e); - continue; - } - if (sqdist < (SUN_RADIUS * SUN_RADIUS)) - { - e->damage += 10.0f * dt; - } - cpVect g = cpvmult(p, -SUN_GRAVITY_STRENGTH / (sqdist * cpfsqrt(sqdist))); - - cpBodyUpdateVelocity(e->body, g, 1.0f, dt); - } - - if (e->is_explosion) - { - e->explosion_progresss += dt; - e->explosion_pos = V2add(e->explosion_pos, V2scale(e->explosion_vel, dt)); - do_explosion(gs, e, dt); - if (e->explosion_progresss >= EXPLOSION_TIME) - { - entity_destroy(gs, e); - } - } - - if (e->is_box) - { - if (e->box_type == BoxExplosive && e->damage >= EXPLOSION_DAMAGE_THRESHOLD) - { - Entity* explosion = new_entity(gs); - explosion->is_explosion = true; - explosion->explosion_pos = entity_pos(e); - explosion->explosion_vel = grid_vel(box_grid(e)); - grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); - } - if (e->damage >= 1.0f) - { - grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); - } - } - if (e->is_grid) - { - // calculate how much energy solar panels provide - float energy_to_add = 0.0f; - BOXES_ITER(gs, cur, e) - { - if (cur->box_type == BoxSolarPanel) - { - cur->sun_amount = clamp01(V2dot(box_facing_vector(cur), V2normalize(V2sub(SUN_POS, entity_pos(cur))))); - energy_to_add += cur->sun_amount * SOLAR_ENERGY_PER_SECOND * dt; - } - } - - // apply all of the energy to all connected batteries - BOXES_ITER(gs, cur, e) - { - if (energy_to_add <= 0.0f) - break; - if (cur->box_type == BoxBattery) - { - float energy_sucked_up_by_battery = cur->energy_used < energy_to_add ? cur->energy_used : energy_to_add; - cur->energy_used -= energy_sucked_up_by_battery; - energy_to_add -= energy_sucked_up_by_battery; - } - assert(energy_to_add >= 0.0f); - } - - // use the energy, stored in the batteries, in various boxes - BOXES_ITER(gs, cur, e) - { - if (cur->box_type == BoxThruster) - { - float energy_to_consume = cur->wanted_thrust * THRUSTER_ENERGY_USED_PER_SECOND * dt; - cur->thrust = 0.0f; - if (possibly_use_energy(gs, e, energy_to_consume)) - { - cur->thrust = cur->wanted_thrust; - cpBodyApplyForceAtWorldPoint(e->body, v2_to_cp(thruster_force(cur)), v2_to_cp(entity_pos(cur))); - } - } - if (cur->box_type == BoxMedbay) - { - Entity* potential_meatbag_to_heal = get_entity(gs, cur->player_who_is_inside_of_me); - if (potential_meatbag_to_heal != NULL) - { - float energy_to_recharge = fminf(potential_meatbag_to_heal->damage, PLAYER_ENERGY_RECHARGE_PER_SECOND * dt); - if (possibly_use_energy(gs, e, energy_to_recharge)) - { - potential_meatbag_to_heal->damage -= energy_to_recharge; - } - } - } - } - } - } - - cpSpaceStep(gs->space, dt); + if (p->damage >= 1.0f) + { + entity_destroy(gs, p); + player->entity = (EntityID){0}; + } + + p->damage = clamp01(p->damage); + } + + if (get_entity(gs, gs->cur_spacestation) == NULL) + { + gs->cur_spacestation = create_spacestation(gs); + } + + // process entities + for (size_t i = 0; i < gs->cur_next_entity; i++) + { + Entity *e = &gs->entities[i]; + if (!e->exists) + continue; + + if (e->is_explosion_unlock) + { + PLAYERS_ITER(gs->players, player) + { + Entity *player_entity = get_entity(gs, player->entity); + if (player_entity != NULL && V2length(V2sub(entity_pos(player_entity), entity_pos(e))) < GOLD_UNLOCK_RADIUS) + { + player->unlocked_bombs = true; + } + } + } + + if (e->body != NULL) + { + cpVect p = cpvsub(cpBodyGetPosition(e->body), v2_to_cp(SUN_POS)); + cpFloat sqdist = cpvlengthsq(p); + if (sqdist > (INSTANT_DEATH_DISTANCE_FROM_SUN * INSTANT_DEATH_DISTANCE_FROM_SUN)) + { + entity_destroy(gs, e); + continue; + } + if (sqdist < (SUN_RADIUS * SUN_RADIUS)) + { + e->damage += 10.0f * dt; + } + cpVect g = cpvmult(p, -SUN_GRAVITY_STRENGTH / (sqdist * cpfsqrt(sqdist))); + + cpBodyUpdateVelocity(e->body, g, 1.0f, dt); + } + + if (e->is_explosion) + { + e->explosion_progresss += dt; + e->explosion_pos = V2add(e->explosion_pos, V2scale(e->explosion_vel, dt)); + do_explosion(gs, e, dt); + if (e->explosion_progresss >= EXPLOSION_TIME) + { + entity_destroy(gs, e); + } + } + + if (e->is_box) + { + if (e->box_type == BoxExplosive && e->damage >= EXPLOSION_DAMAGE_THRESHOLD) + { + Entity *explosion = new_entity(gs); + explosion->is_explosion = true; + explosion->explosion_pos = entity_pos(e); + explosion->explosion_vel = grid_vel(box_grid(e)); + grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); + } + if (e->damage >= 1.0f) + { + grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); + } + } + if (e->is_grid) + { + // calculate how much energy solar panels provide + float energy_to_add = 0.0f; + BOXES_ITER(gs, cur, e) + { + if (cur->box_type == BoxSolarPanel) + { + cur->sun_amount = clamp01(V2dot(box_facing_vector(cur), V2normalize(V2sub(SUN_POS, entity_pos(cur))))); + energy_to_add += cur->sun_amount * SOLAR_ENERGY_PER_SECOND * dt; + } + } + + // apply all of the energy to all connected batteries + BOXES_ITER(gs, cur, e) + { + if (energy_to_add <= 0.0f) + break; + if (cur->box_type == BoxBattery) + { + float energy_sucked_up_by_battery = cur->energy_used < energy_to_add ? cur->energy_used : energy_to_add; + cur->energy_used -= energy_sucked_up_by_battery; + energy_to_add -= energy_sucked_up_by_battery; + } + assert(energy_to_add >= 0.0f); + } + + // use the energy, stored in the batteries, in various boxes + BOXES_ITER(gs, cur, e) + { + if (cur->box_type == BoxThruster) + { + float energy_to_consume = cur->wanted_thrust * THRUSTER_ENERGY_USED_PER_SECOND * dt; + cur->thrust = 0.0f; + if (possibly_use_energy(gs, e, energy_to_consume)) + { + cur->thrust = cur->wanted_thrust; + cpBodyApplyForceAtWorldPoint(e->body, v2_to_cp(thruster_force(cur)), v2_to_cp(entity_pos(cur))); + } + } + if (cur->box_type == BoxMedbay) + { + Entity *potential_meatbag_to_heal = get_entity(gs, cur->player_who_is_inside_of_me); + if (potential_meatbag_to_heal != NULL) + { + float energy_to_recharge = fminf(potential_meatbag_to_heal->damage, PLAYER_ENERGY_RECHARGE_PER_SECOND * dt); + if (possibly_use_energy(gs, e, energy_to_recharge)) + { + potential_meatbag_to_heal->damage -= energy_to_recharge; + } + } + } + } + } + } + + cpSpaceStep(gs->space, dt); } diff --git a/main.c b/main.c index 7e9a905..48953bb 100644 --- a/main.c +++ b/main.c @@ -7,25 +7,25 @@ #include #include // starting server thread -#pragma warning(disable : 33010) // this warning is so broken, doesn't understand assert() +#pragma warning(disable : 33010) // this warning is so broken, doesn't + // understand assert() #include "sokol_app.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "sokol_gp.h" #include "sokol_time.h" #pragma warning(default : 33010) -#pragma warning(disable : 6262) // warning about using a lot of stack, lol that's how stb image is +#pragma warning(disable : 6262) // warning about using a lot of stack, lol + // that's how stb image is #define STB_IMAGE_IMPLEMENTATION -#include "stb_image.h" -#include "types.h" -#include "queue.h" - -#include "opus.h" - #include #include // errno error message on file open #include "minilzo.h" +#include "opus.h" +#include "queue.h" +#include "stb_image.h" +#include "types.h" #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" @@ -34,46 +34,53 @@ #include "hueshift.gen.h" static sg_pipeline pip; -static struct GameState gs = { 0 }; +static struct GameState gs = {0}; static int my_player_index = -1; static bool right_mouse_down = false; #define MAX_KEYDOWN SAPP_KEYCODE_MENU -static bool keydown[MAX_KEYDOWN] = { 0 }; +static bool keydown[MAX_KEYDOWN] = {0}; typedef struct KeyPressed { - bool pressed; - uint64_t frame; + bool pressed; + uint64_t frame; } KeyPressed; -static KeyPressed keypressed[MAX_KEYDOWN] = { 0 }; -static V2 mouse_pos = { 0 }; +static KeyPressed keypressed[MAX_KEYDOWN] = {0}; +static V2 mouse_pos = {0}; static bool fullscreened = false; static bool build_pressed = false; static bool interact_pressed = false; -#define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE+1) -static bool mousedown[MAX_MOUSEBUTTON] = { 0 }; +#define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1) +static bool mousedown[MAX_MOUSEBUTTON] = {0}; typedef struct MousePressed { - bool pressed; - uint64_t frame; + bool pressed; + uint64_t frame; } MousePressed; -static MousePressed mousepressed[MAX_MOUSEBUTTON] = { 0 }; -static EntityID maybe_inviting_this_player = { 0 }; +static MousePressed mousepressed[MAX_MOUSEBUTTON] = {0}; +static EntityID maybe_inviting_this_player = {0}; bool confirm_invite_this_player = false; bool accept_invite = false; bool reject_invite = false; -static V2 camera_pos = { 0 }; // it being a global variable keeps camera at same position after player death +static V2 camera_pos = {0}; // it being a global variable keeps camera at same +// position after player death static float player_scaling = 1.0f; static bool mouse_frozen = false; // @BeforeShip make this debug only thing -static float funval = 0.0f; // easy to play with value controlled by left mouse button when held +static float funval = + 0.0f; // easy to play with value controlled by left mouse button when held // down @BeforeShip remove on release builds -static struct ClientToServer client_to_server = { 0 }; // buffer of inputs -static ENetHost* client; -static ENetPeer* peer; +static Queue input_queue = {0}; +char input_queue_data[QUEUE_SIZE_FOR_ELEMENTS(sizeof(InputFrame), + LOCAL_INPUT_QUEUE_MAX)] = {0}; +static ENetHost *client; +static ENetPeer *peer; static float zoom_target = 300.0f; static float zoom = 300.0f; -static enum Squad take_over_squad = (enum Squad)-1; // -1 means not taking over any squad +static enum Squad take_over_squad = + (enum Squad) - 1; // -1 means not taking over any squad +static float target_prediction_time_factor = 1.0f; +static double current_time_ahead_of_server = 0.0; // images static sg_image image_itemframe; @@ -103,1560 +110,1682 @@ static int cur_editing_rotation = 0; static bool muted = false; static ma_device microphone_device; static ma_device speaker_device; -OpusEncoder* enc; -OpusDecoder* dec; -Queue packets_to_send = { 0 }; -char packets_to_send_data[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)]; -Queue packets_to_play = { 0 }; -char packets_to_play_data[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)]; -ma_mutex send_packets_mutex = { 0 }; -ma_mutex play_packets_mutex = { 0 }; +OpusEncoder *enc; +OpusDecoder *dec; +Queue packets_to_send = {0}; +char packets_to_send_data[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), + VOIP_PACKET_BUFFER_SIZE)]; +Queue packets_to_play = {0}; +char packets_to_play_data[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), + VOIP_PACKET_BUFFER_SIZE)]; +ma_mutex send_packets_mutex = {0}; +ma_mutex play_packets_mutex = {0}; // server thread -void* server_thread_handle = 0; -ServerThreadInfo server_info = { 0 }; +void *server_thread_handle = 0; +ServerThreadInfo server_info = {0}; static struct BoxInfo { - enum BoxType type; - const char* image_path; - sg_image image; - bool needs_tobe_unlocked; + enum BoxType type; + const char *image_path; + sg_image image; + bool needs_tobe_unlocked; } boxes[] = { - // if added to here will show up in toolbar, is placeable - { - .type = BoxHullpiece, - .image_path = "loaded/hullpiece.png", - }, - { - .type = BoxThruster, - .image_path = "loaded/thruster.png", - }, - { - .type = BoxBattery, - .image_path = "loaded/battery.png", - }, - { - .type = BoxCockpit, - .image_path = "loaded/cockpit.png", - }, - { - .type = BoxMedbay, - .image_path = "loaded/medbay.png", - }, - { - .type = BoxSolarPanel, - .image_path = "loaded/solarpanel.png", - }, - { - .type = BoxExplosive, - .image_path = "loaded/explosive.png", - .needs_tobe_unlocked = true, - }, + // if added to here will show up in toolbar, is placeable + { + .type = BoxHullpiece, + .image_path = "loaded/hullpiece.png", + }, + { + .type = BoxThruster, + .image_path = "loaded/thruster.png", + }, + { + .type = BoxBattery, + .image_path = "loaded/battery.png", + }, + { + .type = BoxCockpit, + .image_path = "loaded/cockpit.png", + }, + { + .type = BoxMedbay, + .image_path = "loaded/medbay.png", + }, + { + .type = BoxSolarPanel, + .image_path = "loaded/solarpanel.png", + }, + { + .type = BoxExplosive, + .image_path = "loaded/explosive.png", + .needs_tobe_unlocked = true, + }, }; -#define ENTITIES_ITER(cur) for(Entity* cur = gs.entities; cur < gs.entities + gs.cur_next_entity; cur++) if(cur->exists) +#define ENTITIES_ITER(cur) \ + for (Entity *cur = gs.entities; cur < gs.entities + gs.cur_next_entity; \ + cur++) \ + if (cur->exists) #define ARRLEN(arr) (sizeof(arr) / sizeof(*arr)) static struct SquadMeta { - enum Squad squad; - float hue; - bool is_colorless; + enum Squad squad; + float hue; + bool is_colorless; } squad_metas[] = { - { - .squad = SquadNone, - .is_colorless = true, - }, - { - .squad = SquadRed, - .hue = 21.0f / 360.0f, - }, - { - .squad = SquadGreen, - .hue = 111.0f / 360.0f, - }, - { - .squad = SquadBlue, - .hue = 201.0f / 360.0f, - }, - { - .squad = SquadPurple, - .hue = 291.0f / 360.0f, - }, + { + .squad = SquadNone, + .is_colorless = true, + }, + { + .squad = SquadRed, + .hue = 21.0f / 360.0f, + }, + { + .squad = SquadGreen, + .hue = 111.0f / 360.0f, + }, + { + .squad = SquadBlue, + .hue = 201.0f / 360.0f, + }, + { + .squad = SquadPurple, + .hue = 291.0f / 360.0f, + }, }; struct SquadMeta squad_meta(enum Squad squad) { - for (int i = 0; i < ARRLEN(squad_metas); i++) - { - if (squad_metas[i].squad == squad) - return squad_metas[i]; - } - Log("Could not find squad %d!\n", squad); - return (struct SquadMeta) { 0 }; + for (int i = 0; i < ARRLEN(squad_metas); i++) + { + if (squad_metas[i].squad == squad) + return squad_metas[i]; + } + Log("Could not find squad %d!\n", squad); + return (struct SquadMeta){0}; } -struct BoxInfo - boxinfo(enum BoxType type) +struct BoxInfo boxinfo(enum BoxType type) { - for (int i = 0; i < ARRLEN(boxes); i++) - { - if (boxes[i].type == type) - return boxes[i]; - } - Log("No box info found for type %d\n", type); - return (struct BoxInfo) { 0 }; + for (int i = 0; i < ARRLEN(boxes); i++) + { + if (boxes[i].type == type) + return boxes[i]; + } + Log("No box info found for type %d\n", type); + return (struct BoxInfo){0}; } -static sg_image -load_image(const char* path) +static sg_image load_image(const char *path) { - sg_image to_return = sg_alloc_image(); - - int x = 0; - int y = 0; - int comp = 0; - const int desired_channels = 4; - stbi_set_flip_vertically_on_load(true); - stbi_uc* image_data = stbi_load(path, &x, &y, &comp, desired_channels); - if (!image_data) - { - fprintf(stderr, "Failed to load %s image: %s\n", path, stbi_failure_reason()); - exit(-1); - } - sg_init_image(to_return, - &(sg_image_desc){.width = x, - .height = y, - .pixel_format = SG_PIXELFORMAT_RGBA8, - .min_filter = SG_FILTER_NEAREST, - .mag_filter = SG_FILTER_NEAREST, - .data.subimage[0][0] = { - .ptr = image_data, - .size = (size_t)(x * y * desired_channels), - }}); - - stbi_image_free(image_data); - - return to_return; + sg_image to_return = sg_alloc_image(); + + int x = 0; + int y = 0; + int comp = 0; + const int desired_channels = 4; + stbi_set_flip_vertically_on_load(true); + stbi_uc *image_data = stbi_load(path, &x, &y, &comp, desired_channels); + if (!image_data) + { + fprintf(stderr, "Failed to load %s image: %s\n", path, + stbi_failure_reason()); + exit(-1); + } + sg_init_image(to_return, + &(sg_image_desc){.width = x, + .height = y, + .pixel_format = SG_PIXELFORMAT_RGBA8, + .min_filter = SG_FILTER_NEAREST, + .mag_filter = SG_FILTER_NEAREST, + .data.subimage[0][0] = { + .ptr = image_data, + .size = (size_t)(x * y * desired_channels), + }}); + + stbi_image_free(image_data); + + return to_return; } -void microphone_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +void microphone_data_callback(ma_device *pDevice, void *pOutput, + const void *pInput, ma_uint32 frameCount) { - assert(frameCount == VOIP_EXPECTED_FRAME_COUNT); - if (peer != NULL) - { - ma_mutex_lock(&send_packets_mutex); - OpusPacket* packet = queue_push_element(&packets_to_send); - if (packet == NULL) - { - queue_clear(&packets_to_send); - packet = queue_push_element(&packets_to_send); - } - assert(packet != NULL); - { - opus_int16 muted_audio[VOIP_EXPECTED_FRAME_COUNT] = { 0 }; - const opus_int16* audio_buffer = (const opus_int16*)pInput; - if (muted) - audio_buffer = muted_audio; - opus_int32 written = opus_encode(enc, audio_buffer, VOIP_EXPECTED_FRAME_COUNT, packet->data, VOIP_PACKET_MAX_SIZE); - packet->length = written; - } - ma_mutex_unlock(&send_packets_mutex); - } - (void)pOutput; + assert(frameCount == VOIP_EXPECTED_FRAME_COUNT); +#if 0 // print audio data + Log("Mic data: "); + for (ma_uint32 i = 0; i < VOIP_EXPECTED_FRAME_COUNT; i++) + { + printf("%d ", ((const opus_int16 *)pInput)[i]); + } + printf("\n"); +#endif + if (peer != NULL) + { + ma_mutex_lock(&send_packets_mutex); + OpusPacket *packet = queue_push_element(&packets_to_send); + if (packet == NULL) + { + queue_clear(&packets_to_send); + packet = queue_push_element(&packets_to_send); + } + assert(packet != NULL); + { + opus_int16 muted_audio[VOIP_EXPECTED_FRAME_COUNT] = {0}; + const opus_int16 *audio_buffer = (const opus_int16 *)pInput; + if (muted) + audio_buffer = muted_audio; + opus_int32 written = + opus_encode(enc, audio_buffer, VOIP_EXPECTED_FRAME_COUNT, + packet->data, VOIP_PACKET_MAX_SIZE); + packet->length = written; + } + ma_mutex_unlock(&send_packets_mutex); + } + (void)pOutput; } -void speaker_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +void speaker_data_callback(ma_device *pDevice, void *pOutput, + const void *pInput, ma_uint32 frameCount) { - assert(frameCount == VOIP_EXPECTED_FRAME_COUNT); - ma_mutex_lock(&play_packets_mutex); - OpusPacket* cur_packet = (OpusPacket*)queue_pop_element(&packets_to_play); - if (cur_packet != NULL) - { - opus_decode(dec, cur_packet->data, cur_packet->length, (opus_int16*)pOutput, frameCount, 0); - } - else - { - opus_decode(dec, NULL, 0, (opus_int16*)pOutput, frameCount, 0); // I think opus makes it sound good if packets are skipped with null - } - ma_mutex_unlock(&play_packets_mutex); - (void)pInput; + assert(frameCount == VOIP_EXPECTED_FRAME_COUNT); + ma_mutex_lock(&play_packets_mutex); + OpusPacket *cur_packet = (OpusPacket *)queue_pop_element(&packets_to_play); + if (cur_packet != NULL) + { + opus_decode(dec, cur_packet->data, cur_packet->length, + (opus_int16 *)pOutput, frameCount, 0); + } + else + { + opus_decode(dec, NULL, 0, (opus_int16 *)pOutput, frameCount, + 0); // I think opus makes it sound good if packets are skipped + // with null + } + ma_mutex_unlock(&play_packets_mutex); + (void)pInput; } -static void -init(void) +static Player *myplayer() { - queue_init(&packets_to_play, sizeof(OpusPacket), packets_to_play_data, ARRLEN(packets_to_play_data)); - queue_init(&packets_to_send, sizeof(OpusPacket), packets_to_send_data, ARRLEN(packets_to_send_data)); - - // audio - { - // opus - { - int error; - enc = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &error); - assert(error == OPUS_OK); - dec = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); - assert(error == OPUS_OK); - } - - ma_device_config microphone_config = ma_device_config_init(ma_device_type_capture); - - microphone_config.capture.format = ma_format_s16; - microphone_config.capture.channels = 1; - microphone_config.sampleRate = VOIP_SAMPLE_RATE; - microphone_config.dataCallback = microphone_data_callback; - - ma_device_config speaker_config = ma_device_config_init(ma_device_type_playback); - speaker_config.playback.format = ma_format_s16; - speaker_config.playback.channels = 1; - speaker_config.sampleRate = VOIP_SAMPLE_RATE; - speaker_config.dataCallback = speaker_data_callback; - - ma_result result; - - result = ma_device_init(NULL, µphone_config, µphone_device); - if (result != MA_SUCCESS) - { - Log("Failed to initialize capture device.\n"); - exit(-1); - } - - result = ma_device_init(NULL, &speaker_config, &speaker_device); - if (result != MA_SUCCESS) - { - ma_device_uninit(µphone_device); - Log("Failed to init speaker\n"); - exit(-1); - } - - if (ma_mutex_init(&send_packets_mutex) != MA_SUCCESS) - Log("Failed to init send mutex\n"); - if (ma_mutex_init(&play_packets_mutex) != MA_SUCCESS) - Log("Failed to init play mutex\n"); - - result = ma_device_start(µphone_device); - if (result != MA_SUCCESS) - { - ma_device_uninit(µphone_device); - Log("Failed to start device.\n"); - exit(-1); - } - - result = ma_device_start(&speaker_device); - if (result != MA_SUCCESS) - { - ma_device_uninit(µphone_device); - ma_device_uninit(&speaker_device); - Log("Failed to start speaker\n"); - exit(-1); - } - - Log("Initialized audio\n"); - } - - // @BeforeShip make all fprintf into logging to file, warning dialog grids on - // failure instead of exit(-1), replace the macros in sokol with this as well, - // like assert - - Entity* entity_data = malloc(sizeof * entity_data * MAX_ENTITIES); - initialize(&gs, entity_data, sizeof * entity_data * MAX_ENTITIES); - - sg_desc sgdesc = { .context = sapp_sgcontext() }; - sg_setup(&sgdesc); - if (!sg_isvalid()) - { - fprintf(stderr, "Failed to create Sokol GFX context!\n"); - exit(-1); - } - - sgp_desc sgpdesc = { 0 }; - sgp_setup(&sgpdesc); - if (!sgp_is_valid()) - { - fprintf(stderr, - "Failed to create Sokol GP context: %s\n", - sgp_get_error_message(sgp_get_last_error())); - exit(-1); - } - - // shaders - { - // initialize shader - sgp_pipeline_desc pip_desc = { - .shader = *hueshift_program_shader_desc(sg_query_backend()), - .blend_mode = SGP_BLENDMODE_BLEND, - }; - pip = sgp_make_pipeline(&pip_desc); - if (sg_query_pipeline_state(pip) != SG_RESOURCESTATE_VALID) - { - fprintf(stderr, "failed to make custom pipeline\n"); - exit(-1); - } - } - - // images loading - { - for (int i = 0; i < ARRLEN(boxes); i++) - { - boxes[i].image = load_image(boxes[i].image_path); - } - image_thrusterburn = load_image("loaded/thrusterburn.png"); - image_itemframe = load_image("loaded/itemframe.png"); - image_itemframe_selected = load_image("loaded/itemframe_selected.png"); - image_player = load_image("loaded/player.png"); - image_cockpit_used = load_image("loaded/cockpit_used.png"); - image_stars = load_image("loaded/stars.png"); - image_stars2 = load_image("loaded/stars2.png"); - image_sun = load_image("loaded/sun.png"); - image_medbay_used = load_image("loaded/medbay_used.png"); - image_mystery = load_image("loaded/mystery.png"); - image_explosion = load_image("loaded/explosion.png"); - image_low_health = load_image("loaded/low_health.png"); - image_mic_muted = load_image("loaded/mic_muted.png"); - image_mic_unmuted = load_image("loaded/mic_unmuted.png"); - image_flag_available = load_image("loaded/flag_available.png"); - image_flag_taken = load_image("loaded/flag_ripped.png"); - image_squad_invite = load_image("loaded/squad_invite.png"); - image_check = load_image("loaded/check.png"); - image_no = load_image("loaded/no.png"); - } - - // socket initialization - { - if (enet_initialize() != 0) - { - fprintf(stderr, "An error occurred while initializing ENet.\n"); - exit(-1); - } - client = enet_host_create(NULL /* create a client host */, - 1 /* only allow 1 outgoing connection */, - 2 /* allow up 2 channels to be used, 0 and 1 */, - 0 /* assume any amount of incoming bandwidth */, - 0 /* assume any amount of outgoing bandwidth */); - if (client == NULL) - { - fprintf( - stderr, - "An error occurred while trying to create an ENet client host.\n"); - exit(-1); - } - ENetAddress address; - ENetEvent event; - - enet_address_set_host(&address, SERVER_ADDRESS); - address.port = SERVER_PORT; - peer = enet_host_connect(client, &address, 2, 0); - if (peer == NULL) - { - fprintf(stderr, - "No available peers for initiating an ENet connection.\n"); - exit(-1); - } - // the timeout is the third parameter here - if (enet_host_service(client, &event, 5000) > 0 && event.type == ENET_EVENT_TYPE_CONNECT) - { - Log("Connected\n"); - } - else - { - /* Either the 5 seconds are up or a disconnect event was */ - /* received. Reset the peer in the event the 5 seconds */ - /* had run out without any significant event. */ - enet_peer_reset(peer); - fprintf(stderr, "Connection to server failed."); - exit(-1); - } - } + if (my_player_index == -1) + return NULL; + return &gs.players[my_player_index]; } -#define transform_scope DeferLoop(sgp_push_transform(), sgp_pop_transform()) - -static void -draw_color_rect_centered(V2 center, float size) +static Entity *myentity() { - float halfbox = size / 2.0f; - - sgp_draw_filled_rect(center.x - halfbox, center.y - halfbox, size, size); + if (myplayer() == NULL) + return NULL; + Entity *to_return = get_entity(&gs, myplayer()->entity); + if (to_return != NULL) + assert(to_return->is_player); + return to_return; } -static void -draw_texture_rectangle_centered(V2 center, V2 width_height) +void recalculate_camera_pos() { - V2 halfsize = V2scale(width_height, 0.5f); - sgp_draw_textured_rect(center.x - halfsize.x, - center.y - halfsize.y, - width_height.x, - width_height.y); + if (myentity() != NULL) + { + camera_pos = entity_pos(myentity()); + } } -static void -draw_texture_centered(V2 center, float size) +static void init(void) { - draw_texture_rectangle_centered(center, (V2) { size, size }); + queue_init(&packets_to_play, sizeof(OpusPacket), packets_to_play_data, + ARRLEN(packets_to_play_data)); + queue_init(&packets_to_send, sizeof(OpusPacket), packets_to_send_data, + ARRLEN(packets_to_send_data)); + queue_init(&input_queue, sizeof(InputFrame), input_queue_data, + ARRLEN(input_queue_data)); + + // audio + { + // opus + { + int error; + enc = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, + &error); + assert(error == OPUS_OK); + dec = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); + assert(error == OPUS_OK); + } + + ma_device_config microphone_config = + ma_device_config_init(ma_device_type_capture); + + microphone_config.capture.format = ma_format_s16; + microphone_config.capture.channels = 1; + microphone_config.sampleRate = VOIP_SAMPLE_RATE; + microphone_config.dataCallback = microphone_data_callback; + + ma_device_config speaker_config = + ma_device_config_init(ma_device_type_playback); + speaker_config.playback.format = ma_format_s16; + speaker_config.playback.channels = 1; + speaker_config.sampleRate = VOIP_SAMPLE_RATE; + speaker_config.dataCallback = speaker_data_callback; + + ma_result result; + + result = ma_device_init(NULL, µphone_config, µphone_device); + if (result != MA_SUCCESS) + { + Log("Failed to initialize capture device.\n"); + exit(-1); + } + + result = ma_device_init(NULL, &speaker_config, &speaker_device); + if (result != MA_SUCCESS) + { + ma_device_uninit(µphone_device); + Log("Failed to init speaker\n"); + exit(-1); + } + + if (ma_mutex_init(&send_packets_mutex) != MA_SUCCESS) + Log("Failed to init send mutex\n"); + if (ma_mutex_init(&play_packets_mutex) != MA_SUCCESS) + Log("Failed to init play mutex\n"); + + result = ma_device_start(µphone_device); + if (result != MA_SUCCESS) + { + ma_device_uninit(µphone_device); + Log("Failed to start device.\n"); + exit(-1); + } + + result = ma_device_start(&speaker_device); + if (result != MA_SUCCESS) + { + ma_device_uninit(µphone_device); + ma_device_uninit(&speaker_device); + Log("Failed to start speaker\n"); + exit(-1); + } + + Log("Initialized audio\n"); + } + + // @BeforeShip make all fprintf into logging to file, warning dialog grids on + // failure instead of exit(-1), replace the macros in sokol with this as well, + // like assert + + Entity *entity_data = malloc(sizeof *entity_data * MAX_ENTITIES); + initialize(&gs, entity_data, sizeof *entity_data * MAX_ENTITIES); + + sg_desc sgdesc = {.context = sapp_sgcontext()}; + sg_setup(&sgdesc); + if (!sg_isvalid()) + { + fprintf(stderr, "Failed to create Sokol GFX context!\n"); + exit(-1); + } + + sgp_desc sgpdesc = {0}; + sgp_setup(&sgpdesc); + if (!sgp_is_valid()) + { + fprintf(stderr, "Failed to create Sokol GP context: %s\n", + sgp_get_error_message(sgp_get_last_error())); + exit(-1); + } + + // shaders + { + // initialize shader + sgp_pipeline_desc pip_desc = { + .shader = *hueshift_program_shader_desc(sg_query_backend()), + .blend_mode = SGP_BLENDMODE_BLEND, + }; + pip = sgp_make_pipeline(&pip_desc); + if (sg_query_pipeline_state(pip) != SG_RESOURCESTATE_VALID) + { + fprintf(stderr, "failed to make custom pipeline\n"); + exit(-1); + } + } + + // images loading + { + for (int i = 0; i < ARRLEN(boxes); i++) + { + boxes[i].image = load_image(boxes[i].image_path); + } + image_thrusterburn = load_image("loaded/thrusterburn.png"); + image_itemframe = load_image("loaded/itemframe.png"); + image_itemframe_selected = load_image("loaded/itemframe_selected.png"); + image_player = load_image("loaded/player.png"); + image_cockpit_used = load_image("loaded/cockpit_used.png"); + image_stars = load_image("loaded/stars.png"); + image_stars2 = load_image("loaded/stars2.png"); + image_sun = load_image("loaded/sun.png"); + image_medbay_used = load_image("loaded/medbay_used.png"); + image_mystery = load_image("loaded/mystery.png"); + image_explosion = load_image("loaded/explosion.png"); + image_low_health = load_image("loaded/low_health.png"); + image_mic_muted = load_image("loaded/mic_muted.png"); + image_mic_unmuted = load_image("loaded/mic_unmuted.png"); + image_flag_available = load_image("loaded/flag_available.png"); + image_flag_taken = load_image("loaded/flag_ripped.png"); + image_squad_invite = load_image("loaded/squad_invite.png"); + image_check = load_image("loaded/check.png"); + image_no = load_image("loaded/no.png"); + } + + // socket initialization + { + if (enet_initialize() != 0) + { + fprintf(stderr, "An error occurred while initializing ENet.\n"); + exit(-1); + } + client = enet_host_create(NULL /* create a client host */, + 1 /* only allow 1 outgoing connection */, + 2 /* allow up 2 channels to be used, 0 and 1 */, + 0 /* assume any amount of incoming bandwidth */, + 0 /* assume any amount of outgoing bandwidth */); + if (client == NULL) + { + fprintf( + stderr, + "An error occurred while trying to create an ENet client host.\n"); + exit(-1); + } + ENetAddress address; + ENetEvent event; + + enet_address_set_host(&address, SERVER_ADDRESS); + address.port = SERVER_PORT; + peer = enet_host_connect(client, &address, 2, 0); + if (peer == NULL) + { + fprintf(stderr, + "No available peers for initiating an ENet connection.\n"); + exit(-1); + } + // the timeout is the third parameter here + if (enet_host_service(client, &event, 5000) > 0 && + event.type == ENET_EVENT_TYPE_CONNECT) + { + Log("Connected\n"); + } + else + { + /* Either the 5 seconds are up or a disconnect event was */ + /* received. Reset the peer in the event the 5 seconds */ + /* had run out without any significant event. */ + enet_peer_reset(peer); + fprintf(stderr, "Connection to server failed."); + exit(-1); + } + } } -static void -draw_circle(V2 point, float radius) +#define transform_scope DeferLoop(sgp_push_transform(), sgp_pop_transform()) + +static void draw_color_rect_centered(V2 center, float size) { -#define POINTS 64 - sgp_line lines[POINTS]; - for (int i = 0; i < POINTS; i++) - { - float progress = (float)i / (float)POINTS; - float next_progress = (float)(i + 1) / (float)POINTS; - lines[i].a = (V2){ .x = cosf(progress * 2.0f * PI) * radius, - .y = sinf(progress * 2.0f * PI) * radius }; - lines[i].b = (V2){ .x = cosf(next_progress * 2.0f * PI) * radius, - .y = sinf(next_progress * 2.0f * PI) * radius }; - lines[i].a = V2add(lines[i].a, point); - lines[i].b = V2add(lines[i].b, point); - } - sgp_draw_lines(lines, POINTS); + float halfbox = size / 2.0f; + + sgp_draw_filled_rect(center.x - halfbox, center.y - halfbox, size, size); } +static void draw_texture_rectangle_centered(V2 center, V2 width_height) +{ + V2 halfsize = V2scale(width_height, 0.5f); + sgp_draw_textured_rect(center.x - halfsize.x, center.y - halfsize.y, + width_height.x, width_height.y); +} -static Player* myplayer() +static void draw_texture_centered(V2 center, float size) { - if (my_player_index == -1) - return NULL; - return &gs.players[my_player_index]; + draw_texture_rectangle_centered(center, (V2){size, size}); } -static Entity* -myentity() +static void draw_circle(V2 point, float radius) { - if (myplayer() == NULL) - return NULL; - Entity* to_return = get_entity(&gs, myplayer()->entity); - if (to_return != NULL) - assert(to_return->is_player); - return to_return; +#define POINTS 64 + sgp_line lines[POINTS]; + for (int i = 0; i < POINTS; i++) + { + float progress = (float)i / (float)POINTS; + float next_progress = (float)(i + 1) / (float)POINTS; + lines[i].a = (V2){.x = cosf(progress * 2.0f * PI) * radius, + .y = sinf(progress * 2.0f * PI) * radius}; + lines[i].b = (V2){.x = cosf(next_progress * 2.0f * PI) * radius, + .y = sinf(next_progress * 2.0f * PI) * radius}; + lines[i].a = V2add(lines[i].a, point); + lines[i].b = V2add(lines[i].b, point); + } + sgp_draw_lines(lines, POINTS); } bool can_build(int i) { - bool allow_building = true; - if (boxinfo((enum BoxType)i).needs_tobe_unlocked) - { - allow_building = false; - if (myplayer() != NULL) - allow_building = myplayer()->unlocked_bombs; - } - return allow_building; + bool allow_building = true; + if (boxinfo((enum BoxType)i).needs_tobe_unlocked) + { + allow_building = false; + if (myplayer() != NULL) + allow_building = myplayer()->unlocked_bombs; + } + return allow_building; } void attempt_to_build(int i) { - if (can_build(i)) - cur_editing_boxtype = i; + if (can_build(i)) + cur_editing_boxtype = i; } static V2 screen_to_world(float width, float height, V2 screen) { - V2 world = screen; - world = V2sub(world, (V2) { .x = width / 2.0f, .y = height / 2.0f }); - world.x /= zoom; - world.y /= -zoom; - world = V2add(world, camera_pos); - return world; + V2 world = screen; + world = V2sub(world, (V2){.x = width / 2.0f, .y = height / 2.0f}); + world.x /= zoom; + world.y /= -zoom; + world = V2add(world, camera_pos); + return world; } static V2 world_to_screen(float width, float height, V2 world) { - V2 screen = world; - screen = V2sub(screen, camera_pos); - screen.x *= zoom; - screen.y *= -zoom; - screen = V2add(screen, (V2) { .x = width / 2.0f, .y = height / 2.0f }); - return screen; + V2 screen = world; + screen = V2sub(screen, camera_pos); + screen.x *= zoom; + screen.y *= -zoom; + screen = V2add(screen, (V2){.x = width / 2.0f, .y = height / 2.0f}); + return screen; } -static void -ui(bool draw, float dt, float width, float height) +static void ui(bool draw, float dt, float width, float height) { - static float cur_opacity = 1.0f; - cur_opacity = lerp(cur_opacity, myentity() != NULL ? 1.0f : 0.0f, dt * 4.0f); - if (cur_opacity <= 0.01f) - { - return; - } - - if (draw) - sgp_push_transform(); - - // draw squad invite - static float invite_y = -200.0f; - static enum Squad draw_as_squad = SquadNone; - static float yes_size = 50.0f; - static float no_size = 50.0f; - { - bool invited = myentity() != NULL && myentity()->squad_invited_to != SquadNone; - float size = 200.0f; - float yes_no_size = 50.0f; - float x_center = 0.75f * width; - float x = x_center - size / 2.0f; - //AABB box = (AABB){ .x = x, .y = invite_y, .width = size, .height = size }; - float yes_x = x - size / 4.0f; - float no_x = x + size / 4.0f; - float buttons_y = invite_y + size / 2.0f; - - bool yes_hovered = invited && V2dist(mouse_pos, (V2) { yes_x, buttons_y }) < yes_size / 2.0f; - bool no_hovered = invited && V2dist(mouse_pos, (V2) { no_x, buttons_y }) < no_size / 2.0f; - - yes_size = lerp(yes_size, yes_hovered ? 75.0f : 50.0f, dt * 9.0f); - no_size = lerp(no_size, no_hovered ? 75.0f : 50.0f, dt * 9.0f); - - if (invited && build_pressed && yes_hovered) accept_invite = true; - if (invited && build_pressed && no_hovered) reject_invite = true; - - if (draw) - { - invite_y = lerp(invite_y, invited ? 50.0f : -200.0f, dt * 5.0f); - - if (invited) draw_as_squad = myentity()->squad_invited_to; - - transform_scope{ - sgp_set_pipeline(pip); - struct SquadMeta meta = squad_meta(draw_as_squad); - hueshift_uniforms_t uniform = { 0 }; - uniform.is_colorless = meta.is_colorless; - uniform.target_hue = meta.hue; - sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); - - sgp_scale_at(1.0f, -1.0f, x, invite_y); // images upside down by default :( - sgp_set_image(0, image_squad_invite); - draw_texture_centered((V2) { x, invite_y }, size); - sgp_reset_image(0); - sgp_reset_pipeline(); - } - - // yes - transform_scope{ - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - sgp_scale_at(1.0f, -1.0f, yes_x, buttons_y); - sgp_set_image(0, image_check); - draw_texture_centered((V2) { yes_x, buttons_y }, yes_size); - sgp_reset_image(0); - } - - // no - transform_scope{ - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - sgp_scale_at(1.0f, -1.0f, no_x, buttons_y); - sgp_set_image(0, image_no); - draw_texture_centered((V2) { no_x, buttons_y }, no_size); - sgp_reset_image(0); - } - - } - } - - // draw maybe inviting - { - Entity* inviting = get_entity(&gs, maybe_inviting_this_player); - if (inviting != NULL && myplayer() != NULL) - { - V2 top_of_head = world_to_screen(width, height, V2add(entity_pos(inviting), (V2) { .y = player_scaling * PLAYER_SIZE.y / 2.0f })); - V2 pos = V2add(top_of_head, (V2) { .y = -30.0f }); - V2 to_mouse = V2sub(mouse_pos, world_to_screen(width, height, entity_pos(inviting))); - bool selecting_to_invite = V2dot(V2normalize(to_mouse), (V2) { 0.0f, -1.0f }) > 0.5f && V2length(to_mouse) > 15.0f; - if (!mousedown[SAPP_MOUSEBUTTON_RIGHT]) - { - if (selecting_to_invite) - confirm_invite_this_player = true; - } - if (draw) - transform_scope{ - const float size = 64.0f; - - if (selecting_to_invite) - { - sgp_set_color(0.5f, 0.5f, 0.5f, 0.4f); - sgp_draw_filled_rect(pos.x - size / 2.0f, pos.y - size / 2.0f, size, size); - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - } - sgp_set_pipeline(pip); - struct SquadMeta meta = squad_meta(myplayer()->squad); - hueshift_uniforms_t uniform = { 0 }; - uniform.is_colorless = meta.is_colorless; - uniform.target_hue = meta.hue; - sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); - - sgp_scale_at(1.0f, -1.0f, pos.x, pos.y); // images upside down by default :( - sgp_set_image(0, image_squad_invite); - draw_texture_centered(pos, size); - sgp_reset_image(0); - sgp_reset_pipeline(); - } - } - } - - // draw flags - static V2 flag_pos[SquadLast] = { 0 }; - static float flag_rot[SquadLast] = { 0 }; - static float flag_scaling_increase[SquadLast] = { 0 }; - static bool choosing_flags = false; - const float flag_padding = 70.0f; - const float center_panel_height = 200.0f; - static float center_panel_width = 0.0f; - const float target_center_panel_width = ((SquadLast)+2) * flag_padding; + static float cur_opacity = 1.0f; + cur_opacity = lerp(cur_opacity, myentity() != NULL ? 1.0f : 0.0f, dt * 4.0f); + if (cur_opacity <= 0.01f) + { + return; + } + + if (draw) + sgp_push_transform(); + + // draw squad invite + static float invite_y = -200.0f; + static enum Squad draw_as_squad = SquadNone; + static float yes_size = 50.0f; + static float no_size = 50.0f; + { + bool invited = + myentity() != NULL && myentity()->squad_invited_to != SquadNone; + float size = 200.0f; + float yes_no_size = 50.0f; + float x_center = 0.75f * width; + float x = x_center - size / 2.0f; + // AABB box = (AABB){ .x = x, .y = invite_y, .width = size, .height = size + // }; + float yes_x = x - size / 4.0f; + float no_x = x + size / 4.0f; + float buttons_y = invite_y + size / 2.0f; + + bool yes_hovered = + invited && V2dist(mouse_pos, (V2){yes_x, buttons_y}) < yes_size / 2.0f; + bool no_hovered = + invited && V2dist(mouse_pos, (V2){no_x, buttons_y}) < no_size / 2.0f; + + yes_size = lerp(yes_size, yes_hovered ? 75.0f : 50.0f, dt * 9.0f); + no_size = lerp(no_size, no_hovered ? 75.0f : 50.0f, dt * 9.0f); + + if (invited && build_pressed && yes_hovered) + accept_invite = true; + if (invited && build_pressed && no_hovered) + reject_invite = true; + + if (draw) + { + invite_y = lerp(invite_y, invited ? 50.0f : -200.0f, dt * 5.0f); + + if (invited) + draw_as_squad = myentity()->squad_invited_to; + + transform_scope + { + sgp_set_pipeline(pip); + struct SquadMeta meta = squad_meta(draw_as_squad); + hueshift_uniforms_t uniform = {0}; + uniform.is_colorless = meta.is_colorless; + uniform.target_hue = meta.hue; + sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); + + sgp_scale_at(1.0f, -1.0f, x, + invite_y); // images upside down by default :( + sgp_set_image(0, image_squad_invite); + draw_texture_centered((V2){x, invite_y}, size); + sgp_reset_image(0); + sgp_reset_pipeline(); + } + + // yes + transform_scope + { + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + sgp_scale_at(1.0f, -1.0f, yes_x, buttons_y); + sgp_set_image(0, image_check); + draw_texture_centered((V2){yes_x, buttons_y}, yes_size); + sgp_reset_image(0); + } + + // no + transform_scope + { + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + sgp_scale_at(1.0f, -1.0f, no_x, buttons_y); + sgp_set_image(0, image_no); + draw_texture_centered((V2){no_x, buttons_y}, no_size); + sgp_reset_image(0); + } + } + } + + // draw maybe inviting + { + Entity *inviting = get_entity(&gs, maybe_inviting_this_player); + if (inviting != NULL && myplayer() != NULL) + { + V2 top_of_head = world_to_screen( + width, height, + V2add(entity_pos(inviting), + (V2){.y = player_scaling * PLAYER_SIZE.y / 2.0f})); + V2 pos = V2add(top_of_head, (V2){.y = -30.0f}); + V2 to_mouse = V2sub(mouse_pos, + world_to_screen(width, height, entity_pos(inviting))); + bool selecting_to_invite = + V2dot(V2normalize(to_mouse), (V2){0.0f, -1.0f}) > 0.5f && + V2length(to_mouse) > 15.0f; + if (!mousedown[SAPP_MOUSEBUTTON_RIGHT]) + { + if (selecting_to_invite) + confirm_invite_this_player = true; + } + if (draw) + transform_scope + { + const float size = 64.0f; + + if (selecting_to_invite) + { + sgp_set_color(0.5f, 0.5f, 0.5f, 0.4f); + sgp_draw_filled_rect(pos.x - size / 2.0f, pos.y - size / 2.0f, size, + size); + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + } + sgp_set_pipeline(pip); + struct SquadMeta meta = squad_meta(myplayer()->squad); + hueshift_uniforms_t uniform = {0}; + uniform.is_colorless = meta.is_colorless; + uniform.target_hue = meta.hue; + sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); + + sgp_scale_at(1.0f, -1.0f, pos.x, + pos.y); // images upside down by default :( + sgp_set_image(0, image_squad_invite); + draw_texture_centered(pos, size); + sgp_reset_image(0); + sgp_reset_pipeline(); + } + } + } + + // draw flags + static V2 flag_pos[SquadLast] = {0}; + static float flag_rot[SquadLast] = {0}; + static float flag_scaling_increase[SquadLast] = {0}; + static bool choosing_flags = false; + const float flag_padding = 70.0f; + const float center_panel_height = 200.0f; + static float center_panel_width = 0.0f; + const float target_center_panel_width = ((SquadLast) + 2) * flag_padding; #define FLAG_ITER(i) for (int i = 0; i < SquadLast; i++) - { - FLAG_ITER(i) - { - V2 target_pos = { 0 }; - float target_rot = 0.0f; - float flag_progress = (float)i / (float)(SquadLast - 1.0f); - if (choosing_flags) - { - target_pos.x = width / 2.0f + lerp(-center_panel_width / 2.0f + flag_padding, center_panel_width / 2.0f - flag_padding, flag_progress); - target_pos.y = height * 0.5f; - target_rot = 0.0f; - } - else - { - target_pos.x = 25.0f; - target_pos.y = 200.0f; - target_rot = lerp(-PI / 3.0f, PI / 3.0f, flag_progress) + PI / 2.0f; - } - flag_pos[i] = V2lerp(flag_pos[i], target_pos, dt * 5.0f); - flag_rot[i] = lerp_angle(flag_rot[i], target_rot, dt * 5.0f); - } - - center_panel_width = lerp(center_panel_width, choosing_flags ? target_center_panel_width : 0.0f, 6.0f * dt); - - // center panel - { - AABB panel_rect = (AABB){ - .x = width / 2.0f - center_panel_width / 2.0f, - .y = height / 2.0f - center_panel_height / 2.0f, - .width = center_panel_width, - .height = center_panel_height, - }; - - if (choosing_flags && build_pressed && !has_point(panel_rect, mouse_pos)) - { - build_pressed = false; - choosing_flags = false; - } - if (draw) - { - sgp_set_color(0.7f, 0.7f, 0.7f, 0.5f); - sgp_draw_filled_rect(panel_rect.x, panel_rect.y, panel_rect.width, panel_rect.height); - } - } - - FLAG_ITER(i) - { - enum Squad this_squad = (enum Squad)i; - bool this_squad_available = true; - if (this_squad != SquadNone) - PLAYERS_ITER(gs.players, other_player) - { - if (other_player->squad == this_squad) - { - this_squad_available = false; - break; - } - } - - float size = 128.0f; - bool hovering = V2dist(mouse_pos, flag_pos[i]) < size * 0.25f && this_squad_available; - - if (!choosing_flags && hovering && build_pressed) - { - choosing_flags = true; - build_pressed = false; - } - - if (this_squad_available && choosing_flags && hovering && build_pressed) - { - take_over_squad = this_squad; - build_pressed = false; - } - - flag_scaling_increase[i] = lerp(flag_scaling_increase[i], hovering ? 0.2f : 0.0f, dt * 9.0f); - - size *= 1.0f + flag_scaling_increase[i]; - - if (draw) - { - transform_scope - { - if (this_squad_available) - { - sgp_set_image(0, image_flag_available); - } - else - { - sgp_set_image(0, image_flag_taken); - } - - sgp_set_pipeline(pip); - struct SquadMeta meta = squad_meta(this_squad); - hueshift_uniforms_t uniform = {0}; - uniform.is_colorless = meta.is_colorless; - uniform.target_hue = meta.hue; - sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); - - sgp_rotate_at(flag_rot[i], flag_pos[i].x, flag_pos[i].y); - sgp_scale_at(1.0f, -1.0f, flag_pos[i].x, flag_pos[i].y); // images upside down by default :( - draw_texture_centered(flag_pos[i], size); - - sgp_reset_image(0); - sgp_reset_pipeline(); - } - } - } - - if (choosing_flags) build_pressed = false; // no more inputs beyond flags when the flag choice modal is open - } + { + FLAG_ITER(i) + { + V2 target_pos = {0}; + float target_rot = 0.0f; + float flag_progress = (float)i / (float)(SquadLast - 1.0f); + if (choosing_flags) + { + target_pos.x = + width / 2.0f + lerp(-center_panel_width / 2.0f + flag_padding, + center_panel_width / 2.0f - flag_padding, + flag_progress); + target_pos.y = height * 0.5f; + target_rot = 0.0f; + } + else + { + target_pos.x = 25.0f; + target_pos.y = 200.0f; + target_rot = lerp(-PI / 3.0f, PI / 3.0f, flag_progress) + PI / 2.0f; + } + flag_pos[i] = V2lerp(flag_pos[i], target_pos, dt * 5.0f); + flag_rot[i] = lerp_angle(flag_rot[i], target_rot, dt * 5.0f); + } + + center_panel_width = + lerp(center_panel_width, + choosing_flags ? target_center_panel_width : 0.0f, 6.0f * dt); + + // center panel + { + AABB panel_rect = (AABB){ + .x = width / 2.0f - center_panel_width / 2.0f, + .y = height / 2.0f - center_panel_height / 2.0f, + .width = center_panel_width, + .height = center_panel_height, + }; + + if (choosing_flags && build_pressed && + !has_point(panel_rect, mouse_pos)) + { + build_pressed = false; + choosing_flags = false; + } + if (draw) + { + sgp_set_color(0.7f, 0.7f, 0.7f, 0.5f); + sgp_draw_filled_rect(panel_rect.x, panel_rect.y, panel_rect.width, + panel_rect.height); + } + } + + FLAG_ITER(i) + { + enum Squad this_squad = (enum Squad)i; + bool this_squad_available = true; + if (this_squad != SquadNone) + PLAYERS_ITER(gs.players, other_player) + { + if (other_player->squad == this_squad) + { + this_squad_available = false; + break; + } + } + + float size = 128.0f; + bool hovering = + V2dist(mouse_pos, flag_pos[i]) < size * 0.25f && this_squad_available; + + if (!choosing_flags && hovering && build_pressed) + { + choosing_flags = true; + build_pressed = false; + } + + if (this_squad_available && choosing_flags && hovering && build_pressed) + { + take_over_squad = this_squad; + build_pressed = false; + } + + flag_scaling_increase[i] = + lerp(flag_scaling_increase[i], hovering ? 0.2f : 0.0f, dt * 9.0f); + + size *= 1.0f + flag_scaling_increase[i]; + + if (draw) + { + transform_scope + { + if (this_squad_available) + { + sgp_set_image(0, image_flag_available); + } + else + { + sgp_set_image(0, image_flag_taken); + } + + sgp_set_pipeline(pip); + struct SquadMeta meta = squad_meta(this_squad); + hueshift_uniforms_t uniform = {0}; + uniform.is_colorless = meta.is_colorless; + uniform.target_hue = meta.hue; + sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); + + sgp_rotate_at(flag_rot[i], flag_pos[i].x, flag_pos[i].y); + sgp_scale_at(1.0f, -1.0f, flag_pos[i].x, + flag_pos[i].y); // images upside down by default :( + draw_texture_centered(flag_pos[i], size); + + sgp_reset_image(0); + sgp_reset_pipeline(); + } + } + } + + if (choosing_flags) + build_pressed = false; // no more inputs beyond flags when the flag + // choice modal is open + } #undef FLAG_ITER - // draw spice bar - if (draw) - { - static float damage = 0.5f; - - if (myentity() != NULL) - { - damage = myentity()->damage; - } - - sgp_set_color(0.5f, 0.5f, 0.5f, cur_opacity); - float margin = width * 0.2f; - float bar_width = width - margin * 2.0f; - float y = height - 150.0f; - sgp_draw_filled_rect(margin, y, bar_width, 30.0f); - sgp_set_color(1.0f, 1.0f, 1.0f, cur_opacity); - sgp_draw_filled_rect( - margin, y, bar_width * (1.0f - damage), 30.0f); - } - - // draw muted - static float toggle_mute_opacity = 0.2f; - const float size = 150.0f; - AABB button = (AABB){ - .x = width - size - 40.0f, - .y = height - size - 40.0f, - .width = size, - .height = size, - }; - bool hovered = has_point(button, mouse_pos); - if (build_pressed && hovered) - { - muted = !muted; - build_pressed = false; - } - if (draw) - { - toggle_mute_opacity = lerp(toggle_mute_opacity, hovered ? 1.0f : 0.2f, 6.0f * dt); - sgp_set_color(1.0f, 1.0f, 1.0f, toggle_mute_opacity); - if (muted) - sgp_set_image(0, image_mic_muted); - else - sgp_set_image(0, image_mic_unmuted); - transform_scope - { - sgp_scale_at(1.0f, -1.0f, button.x + button.width / 2.0f, button.y + button.height / 2.0f); - sgp_draw_textured_rect(button.x, button.y, button.width, button.height); - sgp_reset_image(0); - } - } - - // draw item toolbar - { - float itemframe_width = (float)sg_query_image_info(image_itemframe).width * 2.0f; - float itemframe_height = (float)sg_query_image_info(image_itemframe).height * 2.0f; - float total_width = itemframe_width * ARRLEN(boxes); - float item_width = itemframe_width * 0.75f; - float item_height = itemframe_height * 0.75f; - float item_offset_x = (itemframe_width - item_width) / 2.0f; - float item_offset_y = (itemframe_height - item_height) / 2.0f; - - float x = width / 2.0f - total_width / 2.0f; - float y = height - itemframe_height * 1.5f; - for (int i = 0; i < ARRLEN(boxes); i++) - { - if (has_point( - (AABB) { - .x = x, - .y = y, - .width = itemframe_width, - .height = itemframe_height, - }, - mouse_pos) && - build_pressed) - { - // "handle" mouse pressed - attempt_to_build(i); - build_pressed = false; - } - - if (draw) - { - sgp_set_color(1.0f, 1.0f, 1.0f, cur_opacity); - if (cur_editing_boxtype == i) - { - sgp_set_image(0, image_itemframe_selected); - } - else - { - sgp_set_image(0, image_itemframe); - } - sgp_draw_textured_rect(x, y, itemframe_width, itemframe_height); - struct BoxInfo info = boxinfo((enum BoxType)i); - if (can_build(i)) - { - sgp_set_image(0, info.image); - } - else - { - sgp_set_image(0, image_mystery); - } - transform_scope - { - float item_x = x + item_offset_x; - float item_y = y + item_offset_y; - sgp_scale_at(1.0f, -1.0f, item_x + item_width / 2.0f, item_y + item_height / 2.0f); - // sgp_scale(1.0f, -1.0f); - sgp_draw_textured_rect(item_x, item_y, item_width, item_height); - } - sgp_reset_image(0); - } - x += itemframe_width; - } - } - - if (draw) - sgp_pop_transform(); + // draw spice bar + if (draw) + { + static float damage = 0.5f; + + if (myentity() != NULL) + { + damage = myentity()->damage; + } + + sgp_set_color(0.5f, 0.5f, 0.5f, cur_opacity); + float margin = width * 0.2f; + float bar_width = width - margin * 2.0f; + float y = height - 150.0f; + sgp_draw_filled_rect(margin, y, bar_width, 30.0f); + sgp_set_color(1.0f, 1.0f, 1.0f, cur_opacity); + sgp_draw_filled_rect(margin, y, bar_width * (1.0f - damage), 30.0f); + } + + // draw muted + static float toggle_mute_opacity = 0.2f; + const float size = 150.0f; + AABB button = (AABB){ + .x = width - size - 40.0f, + .y = height - size - 40.0f, + .width = size, + .height = size, + }; + bool hovered = has_point(button, mouse_pos); + if (build_pressed && hovered) + { + muted = !muted; + build_pressed = false; + } + if (draw) + { + toggle_mute_opacity = + lerp(toggle_mute_opacity, hovered ? 1.0f : 0.2f, 6.0f * dt); + sgp_set_color(1.0f, 1.0f, 1.0f, toggle_mute_opacity); + if (muted) + sgp_set_image(0, image_mic_muted); + else + sgp_set_image(0, image_mic_unmuted); + transform_scope + { + sgp_scale_at(1.0f, -1.0f, button.x + button.width / 2.0f, + button.y + button.height / 2.0f); + sgp_draw_textured_rect(button.x, button.y, button.width, button.height); + sgp_reset_image(0); + } + } + + // draw item toolbar + { + float itemframe_width = + (float)sg_query_image_info(image_itemframe).width * 2.0f; + float itemframe_height = + (float)sg_query_image_info(image_itemframe).height * 2.0f; + float total_width = itemframe_width * ARRLEN(boxes); + float item_width = itemframe_width * 0.75f; + float item_height = itemframe_height * 0.75f; + float item_offset_x = (itemframe_width - item_width) / 2.0f; + float item_offset_y = (itemframe_height - item_height) / 2.0f; + + float x = width / 2.0f - total_width / 2.0f; + float y = height - itemframe_height * 1.5f; + for (int i = 0; i < ARRLEN(boxes); i++) + { + if (has_point( + (AABB){ + .x = x, + .y = y, + .width = itemframe_width, + .height = itemframe_height, + }, + mouse_pos) && + build_pressed) + { + // "handle" mouse pressed + attempt_to_build(i); + build_pressed = false; + } + + if (draw) + { + sgp_set_color(1.0f, 1.0f, 1.0f, cur_opacity); + if (cur_editing_boxtype == i) + { + sgp_set_image(0, image_itemframe_selected); + } + else + { + sgp_set_image(0, image_itemframe); + } + sgp_draw_textured_rect(x, y, itemframe_width, itemframe_height); + struct BoxInfo info = boxinfo((enum BoxType)i); + if (can_build(i)) + { + sgp_set_image(0, info.image); + } + else + { + sgp_set_image(0, image_mystery); + } + transform_scope + { + float item_x = x + item_offset_x; + float item_y = y + item_offset_y; + sgp_scale_at(1.0f, -1.0f, item_x + item_width / 2.0f, + item_y + item_height / 2.0f); + // sgp_scale(1.0f, -1.0f); + sgp_draw_textured_rect(item_x, item_y, item_width, item_height); + } + sgp_reset_image(0); + } + x += itemframe_width; + } + } + + if (draw) + sgp_pop_transform(); } static void draw_dots(V2 camera_pos, float gap) { - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - const int num = 100; - for (int x = -num; x < num; x++) - { - for (int y = -num; y < num; y++) - { - V2 star = (V2){ (float)x * gap, (float)y * gap }; - if (V2lengthsqr(V2sub(star, camera_pos)) > VISION_RADIUS * VISION_RADIUS) - continue; - - star.x += hash11(star.x * 100.0f + star.y * 67.0f) * gap; - star.y += hash11(star.y * 93.0f + star.x * 53.0f) * gap; - sgp_draw_point(star.x, star.y); - } - } + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + const int num = 100; + for (int x = -num; x < num; x++) + { + for (int y = -num; y < num; y++) + { + V2 star = (V2){(float)x * gap, (float)y * gap}; + if (V2lengthsqr(V2sub(star, camera_pos)) > VISION_RADIUS * VISION_RADIUS) + continue; + + star.x += hash11(star.x * 100.0f + star.y * 67.0f) * gap; + star.y += hash11(star.y * 93.0f + star.x * 53.0f) * gap; + sgp_draw_point(star.x, star.y); + } + } } - -static void -frame(void) +static V2 get_global_hand_pos(V2 world_mouse_pos, bool *hand_at_arms_length) +{ + if (myentity() == NULL) + return (V2){0}; + + V2 global_hand_pos = V2sub(world_mouse_pos, entity_pos(myentity())); + float hand_len = V2length(global_hand_pos); + if (hand_len > MAX_HAND_REACH) + { + *hand_at_arms_length = true; + hand_len = MAX_HAND_REACH; + } + else + { + *hand_at_arms_length = false; + } + global_hand_pos = V2scale(V2normalize(global_hand_pos), hand_len); + global_hand_pos = V2add(global_hand_pos, entity_pos(myentity())); + return global_hand_pos; +} +static void frame(void) { - float width = (float)sapp_width(), height = (float)sapp_height(); - float ratio = width / height; - double time = sapp_frame_count() * sapp_frame_duration(); - float dt = (float)sapp_frame_duration(); - - // pressed input management - { - for (int i = 0; i < MAX_KEYDOWN; i++) - { - if (keypressed[i].frame < sapp_frame_count()) - { - keypressed[i].pressed = false; - } - } - for (int i = 0; i < MAX_MOUSEBUTTON; i++) - { - if (mousepressed[i].frame < sapp_frame_count()) - { - mousepressed[i].pressed = false; - } - } - } - - build_pressed = mousepressed[SAPP_MOUSEBUTTON_LEFT].pressed; - interact_pressed = mousepressed[SAPP_MOUSEBUTTON_RIGHT].pressed; - - // networking - { - ENetEvent event; - while (true) - { - int enet_status = enet_host_service(client, &event, 0); - if (enet_status > 0) - { - switch (event.type) - { - case ENET_EVENT_TYPE_CONNECT: - { - Log("New client from host %x\n", event.peer->address.host); - break; - } - - case ENET_EVENT_TYPE_RECEIVE: - { - char* decompressed = malloc(sizeof * decompressed * MAX_SERVER_TO_CLIENT); // @Robust no malloc - size_t decompressed_max_len = MAX_SERVER_TO_CLIENT; - assert(LZO1X_MEM_DECOMPRESS == 0); - - ma_mutex_lock(&play_packets_mutex); - ServerToClient msg = (ServerToClient){ - .cur_gs = &gs, - .playback_buffer = &packets_to_play, - }; - int return_value = lzo1x_decompress_safe(event.packet->data, event.packet->dataLength, decompressed, &decompressed_max_len, NULL); - if (return_value == LZO_E_OK) - { - server_to_client_deserialize(&msg, decompressed, decompressed_max_len, false); - my_player_index = msg.your_player; - } - else - { - Log("Couldn't decompress gamestate packet, error code %d from lzo\n", return_value); - } - ma_mutex_unlock(&play_packets_mutex); - free(decompressed); - enet_packet_destroy(event.packet); - break; - } - - case ENET_EVENT_TYPE_DISCONNECT: - { - fprintf(stderr, "Disconnected from server\n"); - exit(-1); - break; - } - } - } - else if (enet_status == 0) - { - break; - } - else if (enet_status < 0) - { - fprintf(stderr, "Error receiving enet events: %d\n", enet_status); - break; - } - } - } - - // gameplay - ui(false, dt, width, height); // handle events - V2 build_target_pos = { 0 }; - float build_target_rotation = 0.0f; - V2 world_mouse_pos = screen_to_world(width, height, mouse_pos); // processed later in scope - struct BuildPreviewInfo - { - V2 grid_pos; - float grid_rotation; - } build_preview = { 0 }; - V2 hand_pos = { 0 }; // in local space of grid when hovering over a grid - bool hand_at_arms_length = false; - { - // interpolate zoom - zoom = lerp(zoom, zoom_target, dt * 12.0f); - - // calculate world position and camera - { - if (myentity() != NULL) - { - camera_pos = entity_pos(myentity()); - } - } - - // calculate build preview stuff - EntityID grid_to_build_on = (EntityID){ 0 }; - V2 possibly_local_hand_pos = (V2){ 0 }; - if (myentity() != NULL) - { - hand_pos = V2sub(world_mouse_pos, entity_pos(myentity())); - float hand_len = V2length(hand_pos); - if (hand_len > MAX_HAND_REACH) - { - hand_at_arms_length = true; - hand_len = MAX_HAND_REACH; - } - else - { - hand_at_arms_length = false; - } - hand_pos = V2scale(V2normalize(hand_pos), hand_len); - hand_pos = V2add(hand_pos, entity_pos(myentity())); - - possibly_local_hand_pos = V2sub(hand_pos, entity_pos(myentity())); - Entity* placing_grid = closest_to_point_in_radius(&gs, hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP); - if (placing_grid == NULL) - { - build_preview = (struct BuildPreviewInfo){ - .grid_pos = hand_pos, - .grid_rotation = 0.0f, - }; - } - else - { - grid_to_build_on = get_id(&gs, placing_grid); - hand_pos = grid_snapped_box_pos(placing_grid, hand_pos); - possibly_local_hand_pos = grid_world_to_local(placing_grid, hand_pos); - build_preview = (struct BuildPreviewInfo){ - .grid_pos = entity_pos(placing_grid), - .grid_rotation = entity_rotation(placing_grid), - }; - } - } - - // process player interaction (squad invites) - if (interact_pressed && myplayer() != NULL && myplayer()->squad != SquadNone) - ENTITIES_ITER(cur) - { - if (cur != myentity() && cur->is_player && has_point(centered_at(entity_pos(cur), V2scale(PLAYER_SIZE, player_scaling)), world_mouse_pos)) - { - maybe_inviting_this_player = get_id(&gs, cur); - interact_pressed = false; - } - } - - // Create and send input packet - { - - static size_t last_frame_id = 0; - InputFrame cur_input_frame = { 0 }; - cur_input_frame.id = last_frame_id; - V2 input = (V2){ - .x = (float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A], - .y = (float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S], - }; - if (V2length(input) > 0.0) - input = V2normalize(input); - cur_input_frame.movement = input; - cur_input_frame.seat_action = interact_pressed; - cur_input_frame.grid_hand_pos_local_to = grid_to_build_on; - cur_input_frame.hand_pos = possibly_local_hand_pos; - if (take_over_squad >= 0) - { - cur_input_frame.take_over_squad = take_over_squad; - take_over_squad = -1; - } - else { - cur_input_frame.take_over_squad = -1; // @Robust make this zero initialized - } - if (confirm_invite_this_player) - { - cur_input_frame.invite_this_player = maybe_inviting_this_player; - maybe_inviting_this_player = (EntityID){ 0 }; - } - if (accept_invite) { - cur_input_frame.accept_cur_squad_invite = true; - accept_invite = false; - } - if (reject_invite) - { - cur_input_frame.reject_cur_squad_invite = true; - reject_invite = false; - } - confirm_invite_this_player = false; - - if (build_pressed && cur_editing_boxtype != -1) - { - cur_input_frame.dobuild = build_pressed; - cur_input_frame.build_type = cur_editing_boxtype; - cur_input_frame.build_rotation = cur_editing_rotation; - } - - InputFrame latest = client_to_server.inputs[0]; - // @Robust split this into separate lines and be very careful about testing for inequality - - bool input_differs = false; - input_differs = input_differs || !V2equal(cur_input_frame.movement, latest.movement, 0.01f); - - input_differs = input_differs || cur_input_frame.seat_action != latest.seat_action; - input_differs = input_differs || !entityids_same(cur_input_frame.seat_to_inhabit, latest.seat_to_inhabit); - input_differs = input_differs || !V2equal(cur_input_frame.hand_pos, latest.hand_pos, 0.01f); - - input_differs = input_differs || cur_input_frame.dobuild != latest.dobuild; - input_differs = input_differs || cur_input_frame.build_type != latest.build_type; - input_differs = input_differs || cur_input_frame.build_rotation != latest.build_rotation; - input_differs = input_differs || !entityids_same(cur_input_frame.grid_hand_pos_local_to, latest.grid_hand_pos_local_to); - - input_differs = input_differs || cur_input_frame.accept_cur_squad_invite != latest.accept_cur_squad_invite; - input_differs = input_differs || cur_input_frame.reject_cur_squad_invite != latest.reject_cur_squad_invite; - input_differs = input_differs || !entityids_same(cur_input_frame.invite_this_player, latest.invite_this_player); - input_differs = input_differs || cur_input_frame.take_over_squad != latest.take_over_squad; - - if (input_differs) - { - InputFrame last_frame = client_to_server.inputs[0]; - for (int i = 0; i < INPUT_BUFFER - 1; i++) - { - InputFrame last_last_frame = last_frame; - last_frame = client_to_server.inputs[i + 1]; - client_to_server.inputs[i + 1] = last_last_frame; - - // these references, in old input frames, may have been deleted by the time we - // want to send them. - client_to_server.inputs[i + 1].seat_to_inhabit = cur_input_frame.seat_to_inhabit; - client_to_server.inputs[i + 1].grid_hand_pos_local_to = cur_input_frame.grid_hand_pos_local_to; - } - cur_input_frame.tick = tick(&gs); - client_to_server.inputs[0] = cur_input_frame; - last_frame_id += 1; - } - - static int64_t last_sent_input_time = 0; - if (stm_sec(stm_diff(stm_now(), last_sent_input_time)) > TIME_BETWEEN_INPUT_PACKETS) - { - ma_mutex_lock(&send_packets_mutex); - client_to_server.mic_data = &packets_to_send; - char serialized[MAX_CLIENT_TO_SERVER] = { 0 }; - size_t out_len = 0; - if (client_to_server_serialize(&gs, &client_to_server, serialized, &out_len, MAX_CLIENT_TO_SERVER)) - { - ENetPacket* packet = enet_packet_create((void*)serialized, - out_len, - ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT); - enet_peer_send(peer, 0, packet); // @Robust error check this - last_sent_input_time = stm_now(); - } - else - { - Log("Failed to serialize client to server!\n"); - } - client_to_server.mic_data = NULL; - ma_mutex_unlock(&send_packets_mutex); - } - } - - // @BeforeShip client side prediction and rollback to previous server - // authoritative state, then replay inputs no need to store copies of game - // state, just player input frame to frame. Then know how many frames ago - // the server game state arrived, it's that easy! process(&gs, - // (float)sapp_frame_duration()); - } - - // drawing - { - sgp_begin((int)width, (int)height); - sgp_viewport(0, 0, (int)width, (int)height); - sgp_project(0.0f, width, 0.0f, height); - sgp_set_blend_mode(SGP_BLENDMODE_BLEND); - - // Draw background color - set_color(colhexcode(0x000000)); - // sgp_set_color(0.1f, 0.1f, 0.1f, 1.0f); - sgp_clear(); - - // sokol drawing library draw in world space - // world space coordinates are +Y up, -Y down. Like normal cartesian coords - transform_scope - { - sgp_translate(width / 2, height / 2); - sgp_scale_at(zoom, -zoom, 0.0f, 0.0f); - - // parllax layers, just the zooming, but not 100% of the camera panning + float width = (float)sapp_width(), height = (float)sapp_height(); + float ratio = width / height; + double time = sapp_frame_count() * sapp_frame_duration(); + float dt = (float)sapp_frame_duration(); + + // pressed input management + { + for (int i = 0; i < MAX_KEYDOWN; i++) + { + if (keypressed[i].frame < sapp_frame_count()) + { + keypressed[i].pressed = false; + } + } + for (int i = 0; i < MAX_MOUSEBUTTON; i++) + { + if (mousepressed[i].frame < sapp_frame_count()) + { + mousepressed[i].pressed = false; + } + } + } + + build_pressed = mousepressed[SAPP_MOUSEBUTTON_LEFT].pressed; + interact_pressed = mousepressed[SAPP_MOUSEBUTTON_RIGHT].pressed; + + // networking + { + ENetEvent event; + while (true) + { + int enet_status = enet_host_service(client, &event, 0); + if (enet_status > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + Log("New client from host %x\n", event.peer->address.host); + break; + } + + case ENET_EVENT_TYPE_RECEIVE: + { + char *decompressed = malloc( + sizeof *decompressed * MAX_SERVER_TO_CLIENT); // @Robust no malloc + size_t decompressed_max_len = MAX_SERVER_TO_CLIENT; + assert(LZO1X_MEM_DECOMPRESS == 0); + + ma_mutex_lock(&play_packets_mutex); + double predicted_to_time = gs.time; + ServerToClient msg = (ServerToClient){ + .cur_gs = &gs, + .audio_playback_buffer = &packets_to_play, + }; + int return_value = lzo1x_decompress_safe( + event.packet->data, event.packet->dataLength, decompressed, + &decompressed_max_len, NULL); + if (return_value == LZO_E_OK) + { + server_to_client_deserialize(&msg, decompressed, + decompressed_max_len, false); + my_player_index = msg.your_player; + } + else + { + Log("Couldn't decompress gamestate packet, error code %d from " + "lzo\n", + return_value); + } + ma_mutex_unlock(&play_packets_mutex); + free(decompressed); + enet_packet_destroy(event.packet); + + double server_current_time = gs.time; + double difference = predicted_to_time - server_current_time; + double target_prediction_time = + (((double)peer->roundTripTime) / 1000.0) + TIMESTEP * 6.0; + + // keeps it stable even though causes jumps occasionally + difference = fmax(difference, target_prediction_time); + + double eps = TIMESTEP * 0.1; + if (predicted_to_time - gs.time < target_prediction_time - eps) + { + target_prediction_time_factor = 1.1f; + } + else if (predicted_to_time - gs.time > + target_prediction_time + eps * 2.0) + { + target_prediction_time_factor = 0.9f; + } + else + { + target_prediction_time_factor = 1.0f; + } + + // re-predict the inputs + float time_to_repredict = (float)difference; + if (time_to_repredict > 0.0f) + { + if (time_to_repredict > MAX_REPREDICTION_TIME) + { + Log("Can't repredict all of %f, capping!\n", time_to_repredict); + time_to_repredict = MAX_REPREDICTION_TIME; + } + while (time_to_repredict > TIMESTEP) + { + QUEUE_ITER(&input_queue, cur_header) + { + InputFrame *cur = (InputFrame *)cur_header->data; + if (cur->tick == tick(&gs)) + { + myplayer()->input = *cur; + break; + } + } + process(&gs, TIMESTEP); + time_to_repredict -= TIMESTEP; + } + assert(time_to_repredict <= TIMESTEP); + process(&gs, time_to_repredict); + time_to_repredict = 0.0f; + } + + current_time_ahead_of_server = gs.time - server_current_time; + break; + } + + case ENET_EVENT_TYPE_DISCONNECT: + { + fprintf(stderr, "Disconnected from server\n"); + exit(-1); + break; + } + } + } + else if (enet_status == 0) + { + break; + } + else if (enet_status < 0) + { + fprintf(stderr, "Error receiving enet events: %d\n", enet_status); + break; + } + } + } + + // gameplay + ui(false, dt, width, height); // if ui button is pressed before game logic, set the pressed to + // false so it doesn't propagate from the UI modal/button + V2 build_target_pos = {0}; + float build_target_rotation = 0.0f; + struct BuildPreviewInfo + { + V2 grid_pos; + float grid_rotation; + } build_preview = {0}; + V2 global_hand_pos = {0}; // world coords! world star! + bool hand_at_arms_length = false; + recalculate_camera_pos(); + V2 world_mouse_pos = screen_to_world(width, height, mouse_pos); + { + // interpolate zoom + zoom = lerp(zoom, zoom_target, dt * 12.0f); + + // calculate build preview stuff + V2 local_hand_pos = {0}; + global_hand_pos = + get_global_hand_pos(world_mouse_pos, &hand_at_arms_length); + + if (myentity() != NULL) + { + local_hand_pos = V2sub(global_hand_pos, entity_pos(myentity())); + } + + // process player interaction (squad invites) + if (interact_pressed && myplayer() != NULL && myplayer()->squad != SquadNone) + ENTITIES_ITER(cur) + { + if (cur != myentity() && cur->is_player && + has_point(centered_at(entity_pos(cur), V2scale(PLAYER_SIZE, player_scaling)), world_mouse_pos)) + { + maybe_inviting_this_player = get_id(&gs, cur); + interact_pressed = false; + } + } + + // Create and send input packet, and predict a frame of gamestate + static InputFrame cur_input_frame = { + 0}; // keep across frames for high refresh rate screens + static size_t last_input_committed_tick = 0; + { + // prepare the current input frame, such that when processed next, + // every button/action the player has pressed will be handled + // without frustration by the server. Resulting in authoritative game + // state that looks and feels good. + + V2 input = (V2){ + .x = (float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A], + .y = (float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S], + }; + if (V2length(input) > 0.0) + input = V2normalize(input); + cur_input_frame.movement = input; + + if (interact_pressed) + cur_input_frame.seat_action = interact_pressed; + + cur_input_frame.hand_pos = local_hand_pos; + if (take_over_squad >= 0) + { + cur_input_frame.take_over_squad = take_over_squad; + take_over_squad = -1; + } + if (confirm_invite_this_player) + { + cur_input_frame.invite_this_player = maybe_inviting_this_player; + maybe_inviting_this_player = (EntityID){0}; + confirm_invite_this_player = false; + } + if (accept_invite) + { + cur_input_frame.accept_cur_squad_invite = true; + accept_invite = false; + } + if (reject_invite) + { + cur_input_frame.reject_cur_squad_invite = true; + reject_invite = false; + } + + if (build_pressed && cur_editing_boxtype != -1) + { + cur_input_frame.dobuild = build_pressed; + cur_input_frame.build_type = cur_editing_boxtype; + cur_input_frame.build_rotation = cur_editing_rotation; + } + + // "commit" the input. each input must be on a successive tick. + if (tick(&gs) > last_input_committed_tick) + { + if (cur_input_frame.take_over_squad != -1) + { + Log("Sending take over squad at tick %zu\n", tick(&gs)); + } + cur_input_frame.tick = tick(&gs); + last_input_committed_tick = tick(&gs); + + InputFrame *to_push_to = queue_push_element(&input_queue); + if (to_push_to == NULL) + { + InputFrame *to_discard = queue_pop_element(&input_queue); + (void)to_discard; + to_push_to = queue_push_element(&input_queue); + assert(to_push_to != NULL); + } + + *to_push_to = cur_input_frame; + + if (myplayer() != NULL) + myplayer()->input = + cur_input_frame; // for the client side prediction! + + cur_input_frame = (InputFrame){0}; + cur_input_frame.take_over_squad = + -1; // @Robust make this zero initialized + } + + // in client side prediction, only process the latest in the queue, not + // the one currently constructing. + static float prediction_time_factor = 1.0f; + prediction_time_factor = lerp(prediction_time_factor, + target_prediction_time_factor, dt * 3.0f); + process(&gs, dt * prediction_time_factor); + + static int64_t last_sent_input_time = 0; + if (stm_sec(stm_diff(stm_now(), last_sent_input_time)) > + TIME_BETWEEN_INPUT_PACKETS) + { + ma_mutex_lock(&send_packets_mutex); + ClientToServer to_send = { + .mic_data = &packets_to_send, + .input_data = &input_queue, + }; + char serialized[MAX_CLIENT_TO_SERVER] = {0}; + size_t out_len = 0; + if (client_to_server_serialize(&gs, &to_send, serialized, &out_len, + MAX_CLIENT_TO_SERVER)) + { + char compressed[MAX_CLIENT_TO_SERVER] = {0}; + char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = {0}; + size_t compressed_len = 0; + + lzo1x_1_compress(serialized, out_len, compressed, &compressed_len, + (void *)lzo_working_mem); + + ENetPacket *packet = + enet_packet_create((void *)compressed, compressed_len, + ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT); + enet_peer_send(peer, 0, packet); // @Robust error check this + last_sent_input_time = stm_now(); + } + else + { + Log("Failed to serialize client to server!\n"); + } + ma_mutex_unlock(&send_packets_mutex); + } + } + + // calculate world position and camera + recalculate_camera_pos(); + world_mouse_pos = screen_to_world(width, height, mouse_pos); + global_hand_pos = + get_global_hand_pos(world_mouse_pos, &hand_at_arms_length); + + Entity *placing_grid = closest_to_point_in_radius( + &gs, global_hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP); + if (placing_grid == NULL) + { + build_preview = (struct BuildPreviewInfo){ + .grid_pos = global_hand_pos, + .grid_rotation = 0.0f, + }; + } + else + { + global_hand_pos = grid_snapped_box_pos(placing_grid, global_hand_pos); + build_preview = (struct BuildPreviewInfo){ + .grid_pos = entity_pos(placing_grid), + .grid_rotation = entity_rotation(placing_grid), + }; + } + } + + // drawing + { + sgp_begin((int)width, (int)height); + sgp_viewport(0, 0, (int)width, (int)height); + sgp_project(0.0f, width, 0.0f, height); + sgp_set_blend_mode(SGP_BLENDMODE_BLEND); + + // Draw background color + set_color(colhexcode(0x000000)); + // sgp_set_color(0.1f, 0.1f, 0.1f, 1.0f); + sgp_clear(); + + // WORLD SPACE + // world space coordinates are +Y up, -Y down. Like normal cartesian coords + transform_scope + { + sgp_translate(width / 2, height / 2); + sgp_scale_at(zoom, -zoom, 0.0f, 0.0f); + + // parllax layers, just the zooming, but not 100% of the camera panning #if 1 // space background - transform_scope - { - V2 scaled_camera_pos = V2scale(camera_pos, 0.05f); // this is how strong/weak the parallax is - sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - sgp_set_image(0, image_stars); - float stars_height_over_width = (float)sg_query_image_info(image_stars).height / (float)sg_query_image_info(image_stars).width; - const float stars_width = 35.0f; - float stars_height = stars_width * stars_height_over_width; - sgp_draw_textured_rect(-stars_width / 2.0f, -stars_height / 2.0f, stars_width, stars_height); - // sgp_draw_textured_rect(0, 0, stars_width, stars_height); - sgp_reset_image(0); - } - transform_scope - { - V2 scaled_camera_pos = V2scale(camera_pos, 0.1f); // this is how strong/weak the parallax is - sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - sgp_set_image(0, image_stars2); - float stars_height_over_width = (float)sg_query_image_info(image_stars).height / (float)sg_query_image_info(image_stars).width; - const float stars_width = 35.0f; - float stars_height = stars_width * stars_height_over_width; - sgp_draw_textured_rect(-stars_width / 2.0f, -stars_height / 2.0f, stars_width, stars_height); - // sgp_draw_textured_rect(0, 0, stars_width, stars_height); - sgp_reset_image(0); - } + transform_scope + { + V2 scaled_camera_pos = V2scale( + camera_pos, 0.05f); // this is how strong/weak the parallax is + sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + sgp_set_image(0, image_stars); + float stars_height_over_width = + (float)sg_query_image_info(image_stars).height / + (float)sg_query_image_info(image_stars).width; + const float stars_width = 35.0f; + float stars_height = stars_width * stars_height_over_width; + sgp_draw_textured_rect(-stars_width / 2.0f, -stars_height / 2.0f, + stars_width, stars_height); + // sgp_draw_textured_rect(0, 0, stars_width, stars_height); + sgp_reset_image(0); + } + transform_scope + { + V2 scaled_camera_pos = V2scale( + camera_pos, 0.1f); // this is how strong/weak the parallax is + sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + sgp_set_image(0, image_stars2); + float stars_height_over_width = + (float)sg_query_image_info(image_stars).height / + (float)sg_query_image_info(image_stars).width; + const float stars_width = 35.0f; + float stars_height = stars_width * stars_height_over_width; + sgp_draw_textured_rect(-stars_width / 2.0f, -stars_height / 2.0f, + stars_width, stars_height); + // sgp_draw_textured_rect(0, 0, stars_width, stars_height); + sgp_reset_image(0); + } #endif #if 1 // parallaxed dots - transform_scope - { - V2 scaled_camera_pos = V2scale(camera_pos, 0.25f); - sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - draw_dots(scaled_camera_pos, 3.0f); - } - transform_scope - { - V2 scaled_camera_pos = V2scale(camera_pos, 0.5f); - sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - draw_dots(scaled_camera_pos, 2.0f); - } + transform_scope + { + V2 scaled_camera_pos = V2scale(camera_pos, 0.25f); + sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + draw_dots(scaled_camera_pos, 3.0f); + } + transform_scope + { + V2 scaled_camera_pos = V2scale(camera_pos, 0.5f); + sgp_translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + draw_dots(scaled_camera_pos, 2.0f); + } #endif - // camera go to player - sgp_translate(-camera_pos.x, -camera_pos.y); - - draw_dots(camera_pos, 1.5f); // in plane dots - - // hand reached limit circle - if (myentity() != NULL) - { - static float hand_reach_alpha = 1.0f; - hand_reach_alpha = lerp(hand_reach_alpha, hand_at_arms_length ? 1.0f : 0.0f, dt * 5.0f); - sgp_set_color(1.0f, 1.0f, 1.0f, hand_reach_alpha); - draw_circle(entity_pos(myentity()), MAX_HAND_REACH); - } - - // vision circle, what player can see - if (myentity() != NULL) - { - set_color(colhexcode(0x4685e3)); - draw_circle(entity_pos(myentity()), VISION_RADIUS); - } - - float halfbox = BOX_SIZE / 2.0f; - - // mouse frozen, debugging tool - if (mouse_frozen) - { - sgp_set_color(1.0f, 0.0f, 0.0f, 0.5f); - sgp_draw_filled_rect(world_mouse_pos.x, world_mouse_pos.y, 0.1f, 0.1f); - } - - // building preview - if (cur_editing_boxtype != -1) - { - sgp_set_color(0.5f, 0.5f, 0.5f, (sinf((float)time * 9.0f) + 1.0f) / 3.0f + 0.2f); - - transform_scope - { - sgp_set_image(0, boxinfo(cur_editing_boxtype).image); - sgp_rotate_at(build_preview.grid_rotation + rotangle(cur_editing_rotation), - hand_pos.x, - hand_pos.y); - draw_texture_centered(hand_pos, BOX_SIZE); - // drawbox(hand_pos, build_preview.grid_rotation, 0.0f, - // cur_editing_boxtype, cur_editing_rotation); - sgp_reset_image(0); - } - } - - player_scaling = lerp(player_scaling, zoom < 6.5f ? 100.0f : 1.0f, dt * 7.0f); - ENTITIES_ITER(e) - { - // draw grid - if (e->is_grid) - { - Entity* g = e; - BOXES_ITER(&gs, b, g) - { - if (b->is_explosion_unlock) - { - set_color(colhexcode(0xfcba03)); - draw_circle(entity_pos(b), GOLD_UNLOCK_RADIUS); - } - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - // debug draw force vectors for thrusters - #if 0 - { - if (b->type == BoxThruster) - { - dbg_rect(entity_pos(b)); - dbg_line(entity_pos(b), V2add(entity_pos(b), V2scale(thruster_force(b), -1.0f))); - } - } - #endif - if (b->box_type == BoxBattery) - { - float cur_alpha = sgp_get_color().a; - Color from = WHITE; - Color to = colhex(255, 0, 0); - Color result = Collerp(from, to, b->energy_used / BATTERY_CAPACITY); - sgp_set_color(result.r, result.g, result.b, cur_alpha); - } - transform_scope - { - sgp_rotate_at(entity_rotation(g) + rotangle(b->compass_rotation), - entity_pos(b).x, - entity_pos(b).y); - - if (b->box_type == BoxThruster) - { - transform_scope - { - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - sgp_set_image(0, image_thrusterburn); - // float scaling = 1.0 + (hash11(time*3.0)/2.0)*lerp(0.0, - // 0.07, b->thrust); printf("%f\n", b->thrust); - float scaling = 0.95f + lerp(0.0f, 0.3f, b->thrust); - // float scaling = 1.1; - // sgp_translate(-(scaling*BOX_SIZE - BOX_SIZE), 0.0); - // sgp_scale(scaling, 1.0); - sgp_scale_at(scaling, 1.0f, entity_pos(b).x, entity_pos(b).y); - draw_texture_centered(entity_pos(b), BOX_SIZE); - sgp_reset_image(0); - } - } - sg_image img = boxinfo(b->box_type).image; - if (b->box_type == BoxCockpit) - { - if (get_entity(&gs, b->player_who_is_inside_of_me) != NULL) - img = image_cockpit_used; - } - if (b->box_type == BoxMedbay) - { - if (get_entity(&gs, b->player_who_is_inside_of_me) != NULL) - img = image_medbay_used; - } - sgp_set_image(0, img); - if (b->indestructible) - { - sgp_set_color(0.2f, 0.2f, 0.2f, 1.0f); - } - draw_texture_centered(entity_pos(b), BOX_SIZE); - sgp_reset_image(0); - - if (b->box_type == BoxSolarPanel) - { - Color to_set = colhexcode(0xeb9834); - to_set.a = b->sun_amount * 0.5f; - set_color(to_set); - draw_color_rect_centered(entity_pos(b), BOX_SIZE); - } - - sgp_set_color(0.5f, 0.1f, 0.1f, b->damage); - draw_color_rect_centered(entity_pos(b), BOX_SIZE); - } - } - - // draw the velocity - #if 0 - sgp_set_color(1.0f, 0.0f, 0.0f, 1.0f); - V2 vel = grid_vel(g); - V2 to = V2add(grid_com(g), vel); - sgp_draw_line(grid_com(g).x, grid_com(g).y, to.x, to.y); - #endif - } - - // draw player - if (e->is_player && get_entity(&gs, e->currently_inside_of_box) == NULL) - { - transform_scope - { - - sgp_rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - - sgp_set_pipeline(pip); - struct SquadMeta meta = squad_meta(e->presenting_squad); - hueshift_uniforms_t uniform = {0}; - uniform.is_colorless = meta.is_colorless; - uniform.target_hue = meta.hue; - sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); - sgp_set_image(0, image_player); - draw_texture_rectangle_centered(entity_pos(e), V2scale(PLAYER_SIZE, player_scaling)); - sgp_reset_image(0); - sgp_reset_pipeline(); - } - } - if (e->is_explosion) - { - sgp_set_image(0, image_explosion); - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f - (e->explosion_progresss / EXPLOSION_TIME)); - draw_texture_centered(e->explosion_pos, EXPLOSION_RADIUS * 2.0f); - sgp_reset_image(0); - } - } - - // gold target - set_color(GOLD); - sgp_draw_filled_rect(gs.goldpos.x, gs.goldpos.y, 0.1f, 0.1f); - - // the SUN - transform_scope - { - sgp_translate(SUN_POS.x, SUN_POS.y); - set_color(WHITE); - sgp_set_image(0, image_sun); - draw_texture_centered((V2) { 0 }, SUN_RADIUS * 2.0f); - sgp_reset_image(0); - - // sun DEATH RADIUS - set_color(RED); - draw_circle((V2) { 0 }, INSTANT_DEATH_DISTANCE_FROM_SUN); - } - - sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); - dbg_drawall(); - } // world space transform end - - // low health - if (myentity() != NULL) - { - sgp_set_color(1.0f, 1.0f, 1.0f, myentity()->damage); - sgp_set_image(0, image_low_health); - draw_texture_rectangle_centered((V2) { width / 2.0f, height / 2.0f }, (V2) { width, height }); - sgp_reset_image(0); - } - - // UI drawn in screen space - ui(true, dt, width, height); - } - - sg_pass_action pass_action = { 0 }; - sg_begin_default_pass(&pass_action, (int)width, (int)height); - sgp_flush(); - sgp_end(); - sg_end_pass(); - sg_commit(); + // camera go to player + sgp_translate(-camera_pos.x, -camera_pos.y); + + draw_dots(camera_pos, 1.5f); // in plane dots + + // hand reached limit circle + if (myentity() != NULL) + { + static float hand_reach_alpha = 1.0f; + hand_reach_alpha = lerp(hand_reach_alpha, + hand_at_arms_length ? 1.0f : 0.0f, dt * 5.0f); + sgp_set_color(1.0f, 1.0f, 1.0f, hand_reach_alpha); + draw_circle(entity_pos(myentity()), MAX_HAND_REACH); + } + + // vision circle, what player can see + if (myentity() != NULL) + { + set_color(colhexcode(0x4685e3)); + draw_circle(entity_pos(myentity()), VISION_RADIUS); + } + + float halfbox = BOX_SIZE / 2.0f; + + // mouse frozen, debugging tool + if (mouse_frozen) + { + sgp_set_color(1.0f, 0.0f, 0.0f, 0.5f); + sgp_draw_filled_rect(world_mouse_pos.x, world_mouse_pos.y, 0.1f, 0.1f); + } + + // building preview + if (cur_editing_boxtype != -1) + { + sgp_set_color(0.5f, 0.5f, 0.5f, + (sinf((float)time * 9.0f) + 1.0f) / 3.0f + 0.2f); + + transform_scope + { + sgp_set_image(0, boxinfo(cur_editing_boxtype).image); + sgp_rotate_at(build_preview.grid_rotation + + rotangle(cur_editing_rotation), + global_hand_pos.x, global_hand_pos.y); + draw_texture_centered(global_hand_pos, BOX_SIZE); + // drawbox(hand_pos, build_preview.grid_rotation, 0.0f, + // cur_editing_boxtype, cur_editing_rotation); + sgp_reset_image(0); + } + } + + player_scaling = + lerp(player_scaling, zoom < 6.5f ? 100.0f : 1.0f, dt * 7.0f); + ENTITIES_ITER(e) + { + // draw grid + if (e->is_grid) + { + Entity *g = e; + BOXES_ITER(&gs, b, g) + { + if (b->is_explosion_unlock) + { + set_color(colhexcode(0xfcba03)); + draw_circle(entity_pos(b), GOLD_UNLOCK_RADIUS); + } + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + if (b->box_type == BoxBattery) + { + float cur_alpha = sgp_get_color().a; + Color from = WHITE; + Color to = colhex(255, 0, 0); + Color result = + Collerp(from, to, b->energy_used / BATTERY_CAPACITY); + sgp_set_color(result.r, result.g, result.b, cur_alpha); + } + transform_scope + { + sgp_rotate_at(entity_rotation(g) + rotangle(b->compass_rotation), + entity_pos(b).x, entity_pos(b).y); + + if (b->box_type == BoxThruster) + { + transform_scope + { + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + sgp_set_image(0, image_thrusterburn); + float scaling = 0.95f + lerp(0.0f, 0.3f, b->thrust); + sgp_scale_at(scaling, 1.0f, entity_pos(b).x, entity_pos(b).y); + draw_texture_centered(entity_pos(b), BOX_SIZE); + sgp_reset_image(0); + } + } + sg_image img = boxinfo(b->box_type).image; + if (b->box_type == BoxCockpit) + { + if (get_entity(&gs, b->player_who_is_inside_of_me) != NULL) + img = image_cockpit_used; + } + if (b->box_type == BoxMedbay) + { + if (get_entity(&gs, b->player_who_is_inside_of_me) != NULL) + img = image_medbay_used; + } + sgp_set_image(0, img); + if (b->indestructible) + { + sgp_set_color(0.2f, 0.2f, 0.2f, 1.0f); + } + draw_texture_centered(entity_pos(b), BOX_SIZE); + sgp_reset_image(0); + + if (b->box_type == BoxSolarPanel) + { + Color to_set = colhexcode(0xeb9834); + to_set.a = b->sun_amount * 0.5f; + set_color(to_set); + draw_color_rect_centered(entity_pos(b), BOX_SIZE); + } + + sgp_set_color(0.5f, 0.1f, 0.1f, b->damage); + draw_color_rect_centered(entity_pos(b), BOX_SIZE); + } + } + } + + // draw player + if (e->is_player && + get_entity(&gs, e->currently_inside_of_box) == NULL) + { + transform_scope + { + sgp_rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + + sgp_set_pipeline(pip); + struct SquadMeta meta = squad_meta(e->presenting_squad); + hueshift_uniforms_t uniform = {0}; + uniform.is_colorless = meta.is_colorless; + uniform.target_hue = meta.hue; + sgp_set_uniform(&uniform, sizeof(hueshift_uniforms_t)); + sgp_set_image(0, image_player); + draw_texture_rectangle_centered( + entity_pos(e), V2scale(PLAYER_SIZE, player_scaling)); + sgp_reset_image(0); + sgp_reset_pipeline(); + } + } + if (e->is_explosion) + { + sgp_set_image(0, image_explosion); + sgp_set_color(1.0f, 1.0f, 1.0f, + 1.0f - (e->explosion_progresss / EXPLOSION_TIME)); + draw_texture_centered(e->explosion_pos, EXPLOSION_RADIUS * 2.0f); + sgp_reset_image(0); + } + } + + // gold target + set_color(GOLD); + sgp_draw_filled_rect(gs.goldpos.x, gs.goldpos.y, 0.1f, 0.1f); + + // the SUN + transform_scope + { + sgp_translate(SUN_POS.x, SUN_POS.y); + set_color(WHITE); + sgp_set_image(0, image_sun); + draw_texture_centered((V2){0}, SUN_RADIUS * 2.0f); + sgp_reset_image(0); + + // sun DEATH RADIUS + set_color(RED); + draw_circle((V2){0}, INSTANT_DEATH_DISTANCE_FROM_SUN); + } + + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + dbg_drawall(); + } // world space transform end + + // low health + if (myentity() != NULL) + { + sgp_set_color(1.0f, 1.0f, 1.0f, myentity()->damage); + sgp_set_image(0, image_low_health); + draw_texture_rectangle_centered((V2){width / 2.0f, height / 2.0f}, + (V2){width, height}); + sgp_reset_image(0); + } + + // UI drawn in screen space + ui(true, dt, width, height); + } + + sg_pass_action pass_action = {0}; + sg_begin_default_pass(&pass_action, (int)width, (int)height); + sgp_flush(); + sgp_end(); + sg_end_pass(); + sg_commit(); } void cleanup(void) { - sg_destroy_pipeline(pip); + sg_destroy_pipeline(pip); - ma_mutex_lock(&server_info.info_mutex); - server_info.should_quit = true; - ma_mutex_unlock(&server_info.info_mutex); - WaitForSingleObject(server_thread_handle, INFINITE); + ma_mutex_lock(&server_info.info_mutex); + server_info.should_quit = true; + ma_mutex_unlock(&server_info.info_mutex); + WaitForSingleObject(server_thread_handle, INFINITE); - ma_mutex_uninit(&send_packets_mutex); - ma_mutex_uninit(&play_packets_mutex); + ma_mutex_uninit(&send_packets_mutex); + ma_mutex_uninit(&play_packets_mutex); - ma_device_uninit(µphone_device); - ma_device_uninit(&speaker_device); + ma_device_uninit(µphone_device); + ma_device_uninit(&speaker_device); - opus_encoder_destroy(enc); - opus_decoder_destroy(dec); + opus_encoder_destroy(enc); + opus_decoder_destroy(dec); - destroy(&gs); - free(gs.entities); - sgp_shutdown(); - sg_shutdown(); - enet_deinitialize(); + destroy(&gs); + free(gs.entities); + sgp_shutdown(); + sg_shutdown(); + enet_deinitialize(); - ma_mutex_uninit(&server_info.info_mutex); + ma_mutex_uninit(&server_info.info_mutex); } -void event(const sapp_event* e) +void event(const sapp_event *e) { - switch (e->type) - { - case SAPP_EVENTTYPE_KEY_DOWN: + switch (e->type) + { + case SAPP_EVENTTYPE_KEY_DOWN: #ifdef DEBUG_TOOLS - if (e->key_code == SAPP_KEYCODE_T) - { - mouse_frozen = !mouse_frozen; - } + if (e->key_code == SAPP_KEYCODE_T) + { + mouse_frozen = !mouse_frozen; + } #endif - if (e->key_code == SAPP_KEYCODE_R) - { - cur_editing_rotation += 1; - cur_editing_rotation %= RotationLast; - } - if (e->key_code == SAPP_KEYCODE_F11) - { - fullscreened = !fullscreened; - sapp_toggle_fullscreen(); - } - if (e->key_code == SAPP_KEYCODE_ESCAPE && fullscreened) - { - sapp_toggle_fullscreen(); - fullscreened = false; - } - int key_num = e->key_code - SAPP_KEYCODE_0; - int target_box = key_num - 1; - if (target_box < BoxLast && target_box >= 0) - { - attempt_to_build(target_box); - } - - if (!mouse_frozen) - { - keydown[e->key_code] = true; - if (keypressed[e->key_code].frame == 0) - { - keypressed[e->key_code].pressed = true; - keypressed[e->key_code].frame = e->frame_count; - } - } - break; - case SAPP_EVENTTYPE_KEY_UP: - if (!mouse_frozen) - { - keydown[e->key_code] = false; - keypressed[e->key_code].pressed = false; - - - - keypressed[e->key_code].frame = 0; - } - break; - case SAPP_EVENTTYPE_MOUSE_SCROLL: - zoom_target *= 1.0f + (e->scroll_y / 4.0f) * 0.1f; - zoom_target = clamp(zoom_target, 0.5f, 900.0f); - break; - case SAPP_EVENTTYPE_MOUSE_DOWN: - mousedown[e->mouse_button] = true; - if (mousepressed[e->mouse_button].frame == 0) - { - mousepressed[e->mouse_button].pressed = true; - mousepressed[e->mouse_button].frame = e->frame_count; - } - break; - case SAPP_EVENTTYPE_MOUSE_UP: - mousedown[e->mouse_button] = false; - mousepressed[e->mouse_button].pressed = false; - mousepressed[e->mouse_button].frame = 0; - break; - case SAPP_EVENTTYPE_MOUSE_MOVE: - if (!mouse_frozen) - { - mouse_pos = (V2){ .x = e->mouse_x, .y = e->mouse_y }; - } - if (right_mouse_down) - { - funval += e->mouse_dx; - Log("Funval %f\n", funval); - } - break; - } + if (e->key_code == SAPP_KEYCODE_R) + { + cur_editing_rotation += 1; + cur_editing_rotation %= RotationLast; + } + if (e->key_code == SAPP_KEYCODE_F11) + { + fullscreened = !fullscreened; + sapp_toggle_fullscreen(); + } + if (e->key_code == SAPP_KEYCODE_ESCAPE && fullscreened) + { + sapp_toggle_fullscreen(); + fullscreened = false; + } + int key_num = e->key_code - SAPP_KEYCODE_0; + int target_box = key_num - 1; + if (target_box < BoxLast && target_box >= 0) + { + attempt_to_build(target_box); + } + + if (!mouse_frozen) + { + keydown[e->key_code] = true; + if (keypressed[e->key_code].frame == 0) + { + keypressed[e->key_code].pressed = true; + keypressed[e->key_code].frame = e->frame_count; + } + } + break; + case SAPP_EVENTTYPE_KEY_UP: + if (!mouse_frozen) + { + keydown[e->key_code] = false; + keypressed[e->key_code].pressed = false; + + keypressed[e->key_code].frame = 0; + } + break; + case SAPP_EVENTTYPE_MOUSE_SCROLL: + zoom_target *= 1.0f + (e->scroll_y / 4.0f) * 0.1f; + zoom_target = clamp(zoom_target, 0.5f, 900.0f); + break; + case SAPP_EVENTTYPE_MOUSE_DOWN: + mousedown[e->mouse_button] = true; + if (mousepressed[e->mouse_button].frame == 0) + { + mousepressed[e->mouse_button].pressed = true; + mousepressed[e->mouse_button].frame = e->frame_count; + } + break; + case SAPP_EVENTTYPE_MOUSE_UP: + mousedown[e->mouse_button] = false; + mousepressed[e->mouse_button].pressed = false; + mousepressed[e->mouse_button].frame = 0; + break; + case SAPP_EVENTTYPE_MOUSE_MOVE: + if (!mouse_frozen) + { + mouse_pos = (V2){.x = e->mouse_x, .y = e->mouse_y}; + } + if (right_mouse_down) + { + funval += e->mouse_dx; + Log("Funval %f\n", funval); + } + break; + } } -sapp_desc -sokol_main(int argc, char* argv[]) +sapp_desc sokol_main(int argc, char *argv[]) { - bool hosting = false; - stm_setup(); - ma_mutex_init(&server_info.info_mutex); - server_info.world_save = "debug_world.bin"; - if (argc > 1) - { - server_thread_handle = (void*)_beginthread(server, 0, (void*)&server_info); - hosting = true; - } - (void)argv; - return (sapp_desc) { - .init_cb = init, - .frame_cb = frame, - .cleanup_cb = cleanup, - .width = 640, - .height = 480, - .gl_force_gles2 = true, - .window_title = hosting ? "Flight Hosting" : "Flight Not Hosting", - .icon.sokol_default = true, - .event_cb = event, - .win32_console_attach = true, - .sample_count = 4, // anti aliasing - }; + bool hosting = false; + stm_setup(); + ma_mutex_init(&server_info.info_mutex); + server_info.world_save = "debug_world.bin"; + if (argc > 1) + { + server_thread_handle = + (void *)_beginthread(server, 0, (void *)&server_info); + hosting = true; + } + (void)argv; + return (sapp_desc){ + .init_cb = init, + .frame_cb = frame, + .cleanup_cb = cleanup, + .width = 640, + .height = 480, + .gl_force_gles2 = true, + .window_title = hosting ? "Flight Hosting" : "Flight Not Hosting", + .icon.sokol_default = true, + .event_cb = event, + .win32_console_attach = true, + .sample_count = 4, // anti aliasing + }; } \ No newline at end of file diff --git a/queue.h b/queue.h index 7587263..57f8575 100644 --- a/queue.h +++ b/queue.h @@ -1,112 +1,137 @@ #pragma once +#include + #ifndef QUEUE_ASSERT -void __assert(bool cond, const char* file, int line, const char* cond_string); +void __assert(bool cond, const char *file, int line, const char *cond_string); #define QUEUE_ASSERT(condition) __assert(condition, __FILE__, __LINE__, #condition) #endif -typedef struct QueueElementHeader { - bool exists; - struct QueueElementHeader* next; - char data[]; +typedef struct QueueElementHeader +{ + bool exists; + struct QueueElementHeader *next; + char data[]; } QueueElementHeader; -typedef struct Queue { - char* data; - size_t data_length; // must be a multiple of sizeof(QueueElementHeader) + element_size - size_t element_size; - QueueElementHeader* next; +typedef struct Queue +{ + char *data; + size_t data_length; // must be a multiple of sizeof(QueueElementHeader) + element_size + size_t element_size; + QueueElementHeader *next; } Queue; #define QUEUE_SIZE_FOR_ELEMENTS(element_size, max_elements) ((sizeof(QueueElementHeader) + element_size) * max_elements) -void queue_init(Queue* q, size_t element_size, char* data, size_t data_length); -void queue_clear(Queue* q); -void* queue_push_element(Queue* q); -size_t queue_num_elements(Queue* q); -void* queue_pop_element(Queue* q); +// oldest to newest +#define QUEUE_ITER(q_ptr, cur_header) for (QueueElementHeader *cur_header = (q_ptr)->next; cur_header != NULL; cur_header = cur_header->next) +void queue_init(Queue *q, size_t element_size, char *data, size_t data_length); +void queue_clear(Queue *q); +void *queue_push_element(Queue *q); +size_t queue_num_elements(Queue *q); +void *queue_pop_element(Queue *q); +void *queue_most_recent_element(Queue *q); #ifdef QUEUE_IMPL -void queue_init(Queue* q, size_t element_size, char* data, size_t data_length) +void queue_init(Queue *q, size_t element_size, char *data, size_t data_length) { - q->data = data; - q->data_length = data_length; - q->element_size = element_size; - QUEUE_ASSERT(data_length % (sizeof(QueueElementHeader) + element_size) == 0); + q->data = data; + q->data_length = data_length; + q->element_size = element_size; + QUEUE_ASSERT(data_length % (sizeof(QueueElementHeader) + element_size) == 0); } -void queue_clear(Queue* q) +void queue_clear(Queue *q) { - QUEUE_ASSERT(q->data != NULL); - for (size_t i = 0; i < q->data_length; i++) - { - q->data[i] = 0; - } - q->next = NULL; + QUEUE_ASSERT(q->data != NULL); + for (size_t i = 0; i < q->data_length; i++) + { + q->data[i] = 0; + } + q->next = NULL; } -#define QUEUE_ELEM_ITER(cur) for(QueueElementHeader *cur = (QueueElementHeader*)q->data; (char*)cur < q->data + q->data_length; (char*)cur += (sizeof(QueueElementHeader) + q->element_size)) +#define QUEUE_ELEM_ITER(cur) for (QueueElementHeader *cur = (QueueElementHeader *)q->data; (char *)cur < q->data + q->data_length; (char *)cur += (sizeof(QueueElementHeader) + q->element_size)) // you push an element, get the return value, cast it to your type, and fill it with data. It's that easy! // if it's null the queue is out of space -void* queue_push_element(Queue* q) +void *queue_push_element(Queue *q) { - QUEUE_ASSERT(q->data != NULL); - QueueElementHeader* to_return = NULL; - QUEUE_ELEM_ITER(cur) - { - if (!cur->exists) - { - to_return = cur; - break; - } - } - - // no free packet found in the buffer - if (to_return == NULL) - { - return NULL; - } - else { - to_return->exists = true; - to_return->next = NULL; // very important. - for (size_t i = 0; i < q->element_size; i++) - to_return->data[i] = 0; - - // add to the end of the linked list chain - if (q->next != NULL) - { - QueueElementHeader* cur = q->next; - while (cur->next != NULL) cur = cur->next; - cur->next = to_return; - } - else { - q->next = to_return; - } - - return (void*)to_return->data; - } + QUEUE_ASSERT(q->data != NULL); + QueueElementHeader *to_return = NULL; + QUEUE_ELEM_ITER(cur) + { + if (!cur->exists) + { + to_return = cur; + break; + } + } + + // no free packet found in the buffer + if (to_return == NULL) + { + return NULL; + } + else + { + to_return->exists = true; + to_return->next = NULL; // very important. + for (size_t i = 0; i < q->element_size; i++) + to_return->data[i] = 0; + + // add to the end of the linked list chain + if (q->next != NULL) + { + QueueElementHeader *cur = q->next; + while (cur->next != NULL) + cur = cur->next; + cur->next = to_return; + } + else + { + q->next = to_return; + } + + return (void *)to_return->data; + } } -size_t queue_num_elements(Queue* q) +size_t queue_num_elements(Queue *q) { - QUEUE_ASSERT(q->data != NULL); - size_t to_return = 0; - QUEUE_ELEM_ITER(cur) - if (cur->exists) to_return++; - return to_return; + QUEUE_ASSERT(q->data != NULL); + size_t to_return = 0; + QUEUE_ELEM_ITER(cur) + if (cur->exists) + to_return++; + return to_return; } // returns null if the queue is empty -void* queue_pop_element(Queue* q) +void *queue_pop_element(Queue *q) { - QUEUE_ASSERT(q->data != NULL); - QueueElementHeader* to_return = q->next; - if (q->next != NULL) q->next = q->next->next; - if (to_return != NULL) to_return->exists = false; // jank! - return to_return == NULL ? NULL : (void*)to_return->data; + QUEUE_ASSERT(q->data != NULL); + QueueElementHeader *to_return = q->next; + if (q->next != NULL) + q->next = q->next->next; + if (to_return != NULL) + to_return->exists = false; // jank! + return to_return == NULL ? NULL : (void *)to_return->data; } +void *queue_most_recent_element(Queue *q) +{ + if (q->next == NULL) + return NULL; + else + { + QueueElementHeader *cur = q->next; + while (cur->next != NULL) + cur = cur->next; + return (void *)cur->data; + } +} #undef QUEUE_ELEM_ITER #endif diff --git a/server.c b/server.c index 76a5274..9371187 100644 --- a/server.c +++ b/server.c @@ -1,12 +1,12 @@ +#include "sokol_time.h" #include // initializing bodies #include "types.h" -#include "sokol_time.h" #include -#include +#include #include // int64 printing +#include #include #include // error string -#include #include "minilzo.h" @@ -16,14 +16,14 @@ #define fopen_s(pFile, filename, mode) ((*(pFile)) = fopen((filename), (mode))) == NULL #endif -#define CONNECTED_PEERS(host, cur) \ - for (ENetPeer *cur = host->peers; cur < host->peers + host->peerCount; cur++) \ - if (cur->state == ENET_PEER_STATE_CONNECTED) +#define CONNECTED_PEERS(host, cur) \ + for (ENetPeer *cur = host->peers; cur < host->peers + host->peerCount; cur++) \ + if (cur->state == ENET_PEER_STATE_CONNECTED) #ifdef PROFILING #define SPALL_IMPLEMENTATION -#pragma warning(disable:4996) // spall uses fopen +#pragma warning(disable : 4996) // spall uses fopen #include "spall.h" #define WIN32_LEAN_AND_MEAN @@ -31,20 +31,22 @@ #define NOMINMAX #include // This is slow, if you can use RDTSC and set the multiplier in SpallInit, you'll have far better timing accuracy -double get_time_in_micros() { - static double invfreq; - if (!invfreq) { - LARGE_INTEGER frequency; - QueryPerformanceFrequency(&frequency); - invfreq = 1000000.0 / frequency.QuadPart; - } - LARGE_INTEGER counter; - QueryPerformanceCounter(&counter); - return counter.QuadPart * invfreq; +double get_time_in_micros() +{ + static double invfreq; + if (!invfreq) + { + LARGE_INTEGER frequency; + QueryPerformanceFrequency(&frequency); + invfreq = 1000000.0 / frequency.QuadPart; + } + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + return counter.QuadPart * invfreq; } static SpallProfile spall_ctx; -static SpallBuffer spall_buffer; +static SpallBuffer spall_buffer; #define PROFILE_SCOPE(name) DeferLoop(SpallTraceBeginLenTidPid(&spall_ctx, &spall_buffer, name, sizeof(name) - 1, 0, 0, get_time_in_micros()), SpallTraceEndTidPid(&spall_ctx, &spall_buffer, 0, 0, get_time_in_micros())) #else // PROFILING @@ -54,509 +56,508 @@ static SpallBuffer spall_buffer; #endif // started in a thread from host -void server(void* info_raw) +void server(void *info_raw) { - ServerThreadInfo* info = (ServerThreadInfo*)info_raw; - const char* world_save_name = info->world_save; + ServerThreadInfo *info = (ServerThreadInfo *)info_raw; + const char *world_save_name = info->world_save; #ifdef PROFILING #define BUFFER_SIZE (1 * 1024 * 1024) - spall_ctx = SpallInit("server.spall", 1); - unsigned char* buffer = malloc(BUFFER_SIZE); - spall_buffer = (SpallBuffer){ - .length = BUFFER_SIZE, - .data = buffer, - }; - SpallBufferInit(&spall_ctx, &spall_buffer); + spall_ctx = SpallInit("server.spall", 1); + unsigned char *buffer = malloc(BUFFER_SIZE); + spall_buffer = (SpallBuffer){ + .length = BUFFER_SIZE, + .data = buffer, + }; + SpallBufferInit(&spall_ctx, &spall_buffer); #endif - - struct GameState gs = { 0 }; - size_t entities_size = (sizeof(Entity) * MAX_ENTITIES); - Entity* entity_data = malloc(entities_size); - initialize(&gs, entity_data, entities_size); - Log("Allocated %zu bytes for entities\n", entities_size); - - Queue player_voip_buffers[MAX_PLAYERS] = { 0 }; - size_t player_voip_buffer_size = QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE); - for (int i = 0; i < MAX_PLAYERS; i++) queue_init(&player_voip_buffers[i], sizeof(OpusPacket), calloc(1, player_voip_buffer_size), player_voip_buffer_size); - OpusEncoder* player_encoders[MAX_PLAYERS] = { 0 }; - OpusDecoder* player_decoders[MAX_PLAYERS] = { 0 }; - - // for (int i = 0; i < MAX_PLAYERS; i++) - //{ - // int error = 0; - // player_encoders[i] = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &error); - // if (error != OPUS_OK) Log("Failed to create encoder\n"); - // player_decoders[i] = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); - // if (error != OPUS_OK) Log("Failed to create decoder\n"); - // } - - if (world_save_name != NULL) - { - size_t read_game_data_buffer_size = entities_size; - char* read_game_data = malloc(read_game_data_buffer_size); - - FILE* file = NULL; - fopen_s(&file, (const char*)world_save_name, "rb"); - if (file == NULL) - { - Log("Could not read from data file %s: errno %d\n", (const char*)world_save_name, errno); - } - else - { - size_t actual_length = fread(read_game_data, sizeof(char), entities_size, file); - if (actual_length <= 1) - { - Log("Could only read %zu bytes, error: errno %d\n", actual_length, errno); - exit(-1); - } - Log("Read %zu bytes from save file\n", actual_length); - ServerToClient msg = (ServerToClient){ - .cur_gs = &gs, - }; - server_to_client_deserialize(&msg, read_game_data, actual_length, true); - fclose(file); - } - - free(read_game_data); - } - -#define BOX_AT_TYPE(grid, pos, type) \ - { \ - Entity *box = new_entity(&gs); \ - box_create(&gs, box, grid, pos); \ - box->box_type = type; \ - } + struct GameState gs = {0}; + size_t entities_size = (sizeof(Entity) * MAX_ENTITIES); + Entity *entity_data = malloc(entities_size); + initialize(&gs, entity_data, entities_size); + Log("Allocated %zu bytes for entities\n", entities_size); + + // inputs + Queue player_input_queues[MAX_PLAYERS] = {0}; + size_t input_queue_data_size = QUEUE_SIZE_FOR_ELEMENTS(sizeof(InputFrame), INPUT_QUEUE_MAX); + for (int i = 0; i < MAX_PLAYERS; i++) + queue_init(&player_input_queues[i], sizeof(InputFrame), calloc(1, input_queue_data_size), input_queue_data_size); + + // voip + Queue player_voip_buffers[MAX_PLAYERS] = {0}; + size_t player_voip_buffer_size = QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE); + for (int i = 0; i < MAX_PLAYERS; i++) + queue_init(&player_voip_buffers[i], sizeof(OpusPacket), calloc(1, player_voip_buffer_size), player_voip_buffer_size); + OpusEncoder *player_encoders[MAX_PLAYERS] = {0}; + OpusDecoder *player_decoders[MAX_PLAYERS] = {0}; + + // for (int i = 0; i < MAX_PLAYERS; i++) + //{ + // int error = 0; + // player_encoders[i] = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &error); + // if (error != OPUS_OK) Log("Failed to create encoder\n"); + // player_decoders[i] = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); + // if (error != OPUS_OK) Log("Failed to create decoder\n"); + // } + + if (world_save_name != NULL) + { + size_t read_game_data_buffer_size = entities_size; + char *read_game_data = malloc(read_game_data_buffer_size); + + FILE *file = NULL; + fopen_s(&file, (const char *)world_save_name, "rb"); + if (file == NULL) + { + Log("Could not read from data file %s: errno %d\n", (const char *)world_save_name, errno); + } + else + { + size_t actual_length = fread(read_game_data, sizeof(char), entities_size, file); + if (actual_length <= 1) + { + Log("Could only read %zu bytes, error: errno %d\n", actual_length, errno); + exit(-1); + } + Log("Read %zu bytes from save file\n", actual_length); + ServerToClient msg = (ServerToClient){ + .cur_gs = &gs, + }; + server_to_client_deserialize(&msg, read_game_data, actual_length, true); + fclose(file); + } + + free(read_game_data); + } + +#define BOX_AT_TYPE(grid, pos, type) \ + { \ + Entity *box = new_entity(&gs); \ + box_create(&gs, box, grid, pos); \ + box->box_type = type; \ + } #define BOX_AT(grid, pos) BOX_AT_TYPE(grid, pos, BoxHullpiece) - // one box policy - if (false) - { - Entity* grid = new_entity(&gs); - grid_create(&gs, grid); - entity_set_pos(grid, (V2) { -BOX_SIZE * 2, 0.0f }); - Entity* box = new_entity(&gs); - box_create(&gs, box, grid, (V2) { 0 }); - } - - // rotation test - if (false) - { - Entity* grid = new_entity(&gs); - grid_create(&gs, grid); - entity_set_pos(grid, (V2) { -BOX_SIZE * 2, 0.0f }); - entity_set_rotation(grid, PI / 1.7f); - cpBodySetVelocity(grid->body, cpv(-0.1, 0.0)); - cpBodySetAngularVelocity(grid->body, 1.0f); - - BOX_AT(grid, ((V2){0})); - BOX_AT(grid, ((V2){BOX_SIZE, 0})); - BOX_AT(grid, ((V2){2.0 * BOX_SIZE, 0})); - BOX_AT(grid, ((V2){2.0 * BOX_SIZE, BOX_SIZE})); - BOX_AT(grid, ((V2){0.0 * BOX_SIZE, -BOX_SIZE})); - } - - if (enet_initialize() != 0) - { - fprintf(stderr, "An error occurred while initializing ENet.\n"); - exit(-1); - } - - ENetAddress address; - ENetHost* enet_host; - int sethost = enet_address_set_host_ip(&address, LOCAL_SERVER_ADDRESS); - if (sethost != 0) - { - Log("Fishy return value from set host: %d\n", sethost); - } - /* Bind the server to port 1234. */ - address.port = SERVER_PORT; - enet_host = enet_host_create(&address /* the address to bind the server host to */, - MAX_PLAYERS /* allow up to MAX_PLAYERS clients and/or outgoing connections */, - 2 /* allow up to 2 channels to be used, 0 and 1 */, - 0 /* assume any amount of incoming bandwidth */, - 0 /* assume any amount of outgoing bandwidth */); - if (enet_host == NULL) - { - fprintf(stderr, - "An error occurred while trying to create an ENet server host.\n"); - exit(-1); - } - - Log("Serving on port %d...\n", SERVER_PORT); - ENetEvent event; - uint64_t last_processed_time = stm_now(); - uint64_t last_saved_world_time = stm_now(); - uint64_t last_sent_audio_time = stm_now(); - float audio_time_to_send = 0.0f; - float total_time = 0.0f; - size_t player_to_latest_id_processed[MAX_PLAYERS] = { 0 }; - char* world_save_buffer = malloc(entities_size); - while (true) - { - ma_mutex_lock(&info->info_mutex); - if (info->should_quit) { - ma_mutex_unlock(&info->info_mutex); - break; - } - ma_mutex_unlock(&info->info_mutex); - - // @Speed handle enet messages and simulate gamestate in parallel, then sync... must clone gamestate for this - while (true) - { - int ret = enet_host_service(enet_host, &event, 0); - if (ret == 0) - break; - if (ret < 0) - { - fprintf(stderr, "Enet host service error %d\n", ret); - } - if (ret > 0) - { - switch (event.type) - { - case ENET_EVENT_TYPE_CONNECT: - { - Log("A new client connected from %x:%u.\n", - event.peer->address.host, - event.peer->address.port); - - int64_t player_slot = -1; - for (int i = 0; i < MAX_PLAYERS; i++) - { - if (!gs.players[i].connected) - { - player_slot = i; - break; - } - } - - if (player_slot == -1) - { - enet_peer_disconnect_now(event.peer, 69); - } - else - { - event.peer->data = (void*)player_slot; - gs.players[player_slot] = (struct Player){ 0 }; - gs.players[player_slot].connected = true; - player_to_latest_id_processed[player_slot] = 0; - - int error; - player_encoders[player_slot] = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &error); - if (error != OPUS_OK) - Log("Failed to create encoder: %d\n", error); - player_decoders[player_slot] = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); - if (error != OPUS_OK) - Log("Failed to create decoder: %d\n", error); + // one box policy + if (false) + { + Entity *grid = new_entity(&gs); + grid_create(&gs, grid); + entity_set_pos(grid, (V2){-BOX_SIZE * 2, 0.0f}); + Entity *box = new_entity(&gs); + box_create(&gs, box, grid, (V2){0}); + } + + // rotation test + if (false) + { + Entity *grid = new_entity(&gs); + grid_create(&gs, grid); + entity_set_pos(grid, (V2){-BOX_SIZE * 2, 0.0f}); + entity_set_rotation(grid, PI / 1.7f); + cpBodySetVelocity(grid->body, cpv(-0.1, 0.0)); + cpBodySetAngularVelocity(grid->body, 1.0f); + + BOX_AT(grid, ((V2){0})); + BOX_AT(grid, ((V2){BOX_SIZE, 0})); + BOX_AT(grid, ((V2){2.0 * BOX_SIZE, 0})); + BOX_AT(grid, ((V2){2.0 * BOX_SIZE, BOX_SIZE})); + BOX_AT(grid, ((V2){0.0 * BOX_SIZE, -BOX_SIZE})); + } + + if (enet_initialize() != 0) + { + fprintf(stderr, "An error occurred while initializing ENet.\n"); + exit(-1); + } + + ENetAddress address; + ENetHost *enet_host; + int sethost = enet_address_set_host_ip(&address, LOCAL_SERVER_ADDRESS); + if (sethost != 0) + { + Log("Fishy return value from set host: %d\n", sethost); + } + /* Bind the server to port 1234. */ + address.port = SERVER_PORT; + enet_host = enet_host_create(&address /* the address to bind the server host to */, + MAX_PLAYERS /* allow up to MAX_PLAYERS clients and/or outgoing connections */, + 2 /* allow up to 2 channels to be used, 0 and 1 */, + 0 /* assume any amount of incoming bandwidth */, + 0 /* assume any amount of outgoing bandwidth */); + if (enet_host == NULL) + { + fprintf(stderr, + "An error occurred while trying to create an ENet server host.\n"); + exit(-1); + } + + Log("Serving on port %d...\n", SERVER_PORT); + ENetEvent event; + uint64_t last_processed_time = stm_now(); + uint64_t last_saved_world_time = stm_now(); + uint64_t last_sent_audio_time = stm_now(); + uint64_t last_sent_gamestate_time = stm_now(); + float audio_time_to_send = 0.0f; + float total_time = 0.0f; + char *world_save_buffer = malloc(entities_size); + while (true) + { + ma_mutex_lock(&info->info_mutex); + if (info->should_quit) + { + ma_mutex_unlock(&info->info_mutex); + break; + } + ma_mutex_unlock(&info->info_mutex); + + // @Speed handle enet messages and simulate gamestate in parallel, then sync... must clone gamestate for this + while (true) + { + int ret = enet_host_service(enet_host, &event, 0); + if (ret == 0) + break; + if (ret < 0) + { + fprintf(stderr, "Enet host service error %d\n", ret); + } + if (ret > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + Log("A new client connected from %x:%u.\n", + event.peer->address.host, + event.peer->address.port); + + int64_t player_slot = -1; + for (int i = 0; i < MAX_PLAYERS; i++) + { + if (!gs.players[i].connected) + { + player_slot = i; + break; + } + } + + if (player_slot == -1) + { + enet_peer_disconnect_now(event.peer, 69); + } + else + { + event.peer->data = (void *)player_slot; + gs.players[player_slot] = (struct Player){0}; + gs.players[player_slot].connected = true; + + int error; + player_encoders[player_slot] = opus_encoder_create(VOIP_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &error); + if (error != OPUS_OK) + Log("Failed to create encoder: %d\n", error); + player_decoders[player_slot] = opus_decoder_create(VOIP_SAMPLE_RATE, 1, &error); + if (error != OPUS_OK) + Log("Failed to create decoder: %d\n", error); #ifdef UNLOCK_ALL - gs.players[player_slot].unlocked_bombs = true; + gs.players[player_slot].unlocked_bombs = true; #endif - gs.players[player_slot].squad = SquadPurple; - } - } - break; - - case ENET_EVENT_TYPE_RECEIVE: - { - // Log("A packet of length %zu was received on channel %u.\n", - // event.packet->dataLength, - // event.channelID); - if (event.packet->dataLength == 0) - { - Log("Wtf an empty packet from enet?\n"); - } - else { - int64_t player_slot = (int64_t)event.peer->data; - size_t length = event.packet->dataLength; -#define VOIP_QUEUE_DECL(queue_name, queue_data_name) Queue queue_name = {0}; char queue_data_name[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)] = {0}; queue_init(&queue_name, sizeof(OpusPacket), queue_data_name, QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)) - VOIP_QUEUE_DECL(throwaway_buffer, throwaway_buffer_data); - Queue* buffer_to_fill = &player_voip_buffers[player_slot]; - if (get_entity(&gs, gs.players[player_slot].entity) == NULL) buffer_to_fill = &throwaway_buffer; - struct ClientToServer received = { .mic_data = buffer_to_fill }; - if (!client_to_server_deserialize(&gs, &received, event.packet->data, event.packet->dataLength)) - { - Log("Bad packet from client %d\n", (int)player_slot); - } - else - { - size_t latest_id = player_to_latest_id_processed[player_slot]; - - if (received.inputs[0].id > latest_id) - { - for (int i = INPUT_BUFFER - 1; i >= 0; i--) - { - if (received.inputs[i].tick == 0) // empty input - continue; - if (received.inputs[i].id <= latest_id) - continue; // don't reprocess inputs already processed - InputFrame cur_input = received.inputs[i]; - gs.players[player_slot].input.movement = cur_input.movement; - gs.players[player_slot].input.hand_pos = cur_input.hand_pos; - - // for these "event" inputs, only modify the current input if the event is true. - // while processing the gamestate, will mark it as false once processed. This - // prevents setting the event input to false before it's been processed. - if (cur_input.take_over_squad >= 0) - gs.players[player_slot].input.take_over_squad = cur_input.take_over_squad; - if (cur_input.accept_cur_squad_invite) - gs.players[player_slot].input.accept_cur_squad_invite = cur_input.accept_cur_squad_invite; - if (cur_input.reject_cur_squad_invite) - gs.players[player_slot].input.reject_cur_squad_invite = cur_input.reject_cur_squad_invite; - if (cur_input.invite_this_player.generation > 0) - { - gs.players[player_slot].input.invite_this_player = cur_input.invite_this_player; - } - if (cur_input.seat_action) - { - gs.players[player_slot].input.seat_action = cur_input.seat_action; - gs.players[player_slot].input.grid_hand_pos_local_to = cur_input.grid_hand_pos_local_to; - } - if (cur_input.dobuild) - { - gs.players[player_slot].input.grid_hand_pos_local_to = cur_input.grid_hand_pos_local_to; - gs.players[player_slot].input.dobuild = cur_input.dobuild; - gs.players[player_slot].input.build_type = cur_input.build_type; - gs.players[player_slot].input.build_rotation = cur_input.build_rotation; - } - } - player_to_latest_id_processed[player_slot] = received.inputs[0].id; - } - } - } - /* Clean up the packet now that we're done using it. */ - enet_packet_destroy(event.packet); - } - break; - - case ENET_EVENT_TYPE_DISCONNECT: - { - int player_index = (int)(int64_t)event.peer->data; - Log("%" PRId64 " disconnected player index %d.\n", (int64_t)event.peer->data, player_index); - Entity* player_body = get_entity(&gs, gs.players[player_index].entity); - if (player_body != NULL) - { - entity_destroy(&gs, player_body); - } - opus_encoder_destroy(player_encoders[player_index]); - player_encoders[player_index] = NULL; - opus_decoder_destroy(player_decoders[player_index]); - player_decoders[player_index] = NULL; - gs.players[player_index].connected = false; - queue_clear(&player_voip_buffers[player_index]); - event.peer->data = NULL; - } - break; - - case ENET_EVENT_TYPE_NONE: - { - } - break; - } - } - - } - total_time += (float)stm_sec(stm_diff(stm_now(), last_processed_time)); - last_processed_time = stm_now(); - // @Robost @BeforeShip if can't process quick enough will be stuck being lagged behind, think of a solution for this... - const float max_time = 5.0f * TIMESTEP; - if (total_time > max_time) - { - Log("Abnormally large total time %f, clamping\n", total_time); - total_time = max_time; - } - - bool processed = false; - PROFILE_SCOPE("World Processing") - { - while (total_time > TIMESTEP) - { - processed = true; - process(&gs, TIMESTEP); - total_time -= TIMESTEP; - } - } - - if (world_save_name != NULL && (stm_sec(stm_diff(stm_now(), last_saved_world_time))) > TIME_BETWEEN_WORLD_SAVE) - { - PROFILE_SCOPE("Save World") { - last_saved_world_time = stm_now(); - ServerToClient msg = (ServerToClient){ - .cur_gs = &gs, - }; - size_t out_len = 0; - if (server_to_client_serialize(&msg, world_save_buffer, &out_len, entities_size, NULL, true)) - { - FILE* save_file = NULL; - fopen_s(&save_file, (const char*)world_save_name, "wb"); - if (save_file == NULL) - { - Log("Could not open save file: errno %d\n", errno); - } - else - { - size_t data_written = fwrite(world_save_buffer, sizeof(*world_save_buffer), out_len, save_file); - if (data_written != out_len) - { - Log("Failed to save world data, wanted to write %zu but could only write %zu\n", out_len, data_written); - } - else - { - Log("Saved game world to %s\n", (const char*)world_save_name); - } - fclose(save_file); - } - } - else - { - Log("URGENT: FAILED TO SAVE WORLD FILE!\n"); - } - } - } - - if (processed) - { - PROFILE_SCOPE("send_data") { - static char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = { 0 }; - - audio_time_to_send += (float)stm_sec(stm_diff(stm_now(), last_sent_audio_time)); - last_sent_audio_time = stm_now(); - int num_audio_packets = (int)floor(1.0f / (VOIP_TIME_PER_PACKET / audio_time_to_send)); + gs.players[player_slot].squad = SquadPurple; + } + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + // Log("A packet of length %zu was received on channel %u.\n", + // event.packet->dataLength, + // event.channelID); + if (event.packet->dataLength == 0) + { + Log("Wtf an empty packet from enet?\n"); + } + else + { + int64_t player_slot = (int64_t)event.peer->data; + size_t length = event.packet->dataLength; +#define VOIP_QUEUE_DECL(queue_name, queue_data_name) \ + Queue queue_name = {0}; \ + char queue_data_name[QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)] = {0}; \ + queue_init(&queue_name, sizeof(OpusPacket), queue_data_name, QUEUE_SIZE_FOR_ELEMENTS(sizeof(OpusPacket), VOIP_PACKET_BUFFER_SIZE)) + VOIP_QUEUE_DECL(throwaway_buffer, throwaway_buffer_data); + Queue *buffer_to_fill = &player_voip_buffers[player_slot]; + if (get_entity(&gs, gs.players[player_slot].entity) == NULL) + buffer_to_fill = &throwaway_buffer; + + queue_clear(&player_input_queues[player_slot]); + struct ClientToServer received = {.mic_data = buffer_to_fill, .input_data = &player_input_queues[player_slot]}; + char decompressed[MAX_CLIENT_TO_SERVER] = {0}; + size_t decompressed_max_len = MAX_CLIENT_TO_SERVER; + assert(LZO1X_MEM_DECOMPRESS == 0); + + int return_value = lzo1x_decompress_safe(event.packet->data, event.packet->dataLength, decompressed, &decompressed_max_len, NULL); + + if (return_value == LZO_E_OK) + { + if (!client_to_server_deserialize(&gs, &received, decompressed, decompressed_max_len)) + { + Log("Bad packet from client %d\n", (int)player_slot); + } + } + else + { + Log("Couldn't decompress player packet, error code %d from lzo\n", return_value); + } + } + /* Clean up the packet now that we're done using it. */ + enet_packet_destroy(event.packet); + } + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + int player_index = (int)(int64_t)event.peer->data; + Log("%" PRId64 " disconnected player index %d.\n", (int64_t)event.peer->data, player_index); + Entity *player_body = get_entity(&gs, gs.players[player_index].entity); + if (player_body != NULL) + { + entity_destroy(&gs, player_body); + } + opus_encoder_destroy(player_encoders[player_index]); + player_encoders[player_index] = NULL; + opus_decoder_destroy(player_decoders[player_index]); + player_decoders[player_index] = NULL; + gs.players[player_index].connected = false; + queue_clear(&player_voip_buffers[player_index]); + event.peer->data = NULL; + } + break; + + case ENET_EVENT_TYPE_NONE: + { + } + break; + } + } + } + total_time += (float)stm_sec(stm_diff(stm_now(), last_processed_time)); + last_processed_time = stm_now(); + // @Robost @BeforeShip if can't process quick enough will be stuck being lagged behind, think of a solution for this... + const float max_time = 5.0f * TIMESTEP; + if (total_time > max_time) + { + Log("Abnormally large total time %f, clamping\n", total_time); + total_time = max_time; + } + + PROFILE_SCOPE("World Processing") + { + while (total_time > TIMESTEP) + { + CONNECTED_PEERS(enet_host, cur) + { + int this_player_index = (int)(int64_t)cur->data; + QUEUE_ITER(&player_input_queues[this_player_index], cur_header) + { + InputFrame *cur = (InputFrame *)cur_header->data; + if (cur->tick == tick(&gs)) + { + gs.players[this_player_index].input = *cur; + break; + } + } + } + + process(&gs, TIMESTEP); + total_time -= TIMESTEP; + } + } + + if (world_save_name != NULL && (stm_sec(stm_diff(stm_now(), last_saved_world_time))) > TIME_BETWEEN_WORLD_SAVE) + { + PROFILE_SCOPE("Save World") + { + last_saved_world_time = stm_now(); + ServerToClient msg = (ServerToClient){ + .cur_gs = &gs, + }; + size_t out_len = 0; + if (server_to_client_serialize(&msg, world_save_buffer, &out_len, entities_size, NULL, true)) + { + FILE *save_file = NULL; + fopen_s(&save_file, (const char *)world_save_name, "wb"); + if (save_file == NULL) + { + Log("Could not open save file: errno %d\n", errno); + } + else + { + size_t data_written = fwrite(world_save_buffer, sizeof(*world_save_buffer), out_len, save_file); + if (data_written != out_len) + { + Log("Failed to save world data, wanted to write %zu but could only write %zu\n", out_len, data_written); + } + else + { + Log("Saved game world to %s\n", (const char *)world_save_name); + } + fclose(save_file); + } + } + else + { + Log("URGENT: FAILED TO SAVE WORLD FILE!\n"); + } + } + } + + if (stm_sec(stm_diff(stm_now(), last_sent_gamestate_time)) > TIME_BETWEEN_SEND_GAMESTATE) + { + last_sent_gamestate_time = stm_now(); + PROFILE_SCOPE("send_data") + { + static char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = {0}; + + audio_time_to_send += (float)stm_sec(stm_diff(stm_now(), last_sent_audio_time)); + last_sent_audio_time = stm_now(); + int num_audio_packets = (int)floor(1.0f / (VOIP_TIME_PER_PACKET / audio_time_to_send)); #define MAX_AUDIO_PACKETS_TO_SEND 6 - if (num_audio_packets > MAX_AUDIO_PACKETS_TO_SEND) - { - Log("Wants %d, this is too many packets. Greater than the maximum %d\n", num_audio_packets, MAX_AUDIO_PACKETS_TO_SEND); - num_audio_packets = MAX_AUDIO_PACKETS_TO_SEND; - } - - opus_int16 decoded_audio_packets[MAX_PLAYERS][MAX_AUDIO_PACKETS_TO_SEND][VOIP_EXPECTED_FRAME_COUNT] = { 0 }; - - audio_time_to_send -= num_audio_packets * VOIP_TIME_PER_PACKET; - - // decode what everybody said - CONNECTED_PEERS(enet_host, cur) - { - int this_player_index = (int)(int64_t)cur->data; - for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) - { - opus_int16* to_dump_to = decoded_audio_packets[this_player_index][packet_i]; - OpusPacket* cur_packet = (OpusPacket*)queue_pop_element(&player_voip_buffers[this_player_index]); - if (cur_packet == NULL) - opus_decode(player_decoders[this_player_index], NULL, 0, to_dump_to, VOIP_EXPECTED_FRAME_COUNT, 0); - else - opus_decode(player_decoders[this_player_index], cur_packet->data, cur_packet->length, to_dump_to, VOIP_EXPECTED_FRAME_COUNT, 0); - } - } - - // send gamestate to each player - CONNECTED_PEERS(enet_host, cur) - { - int this_player_index = (int)(int64_t)cur->data; - Entity* this_player_entity = get_entity(&gs, gs.players[this_player_index].entity); - if (this_player_entity == NULL) - continue; - // @Speed don't recreate the packet for every peer, gets expensive copying gamestate over and over again - char* bytes_buffer = malloc(sizeof * bytes_buffer * MAX_SERVER_TO_CLIENT); - char* compressed_buffer = malloc(sizeof * compressed_buffer * MAX_SERVER_TO_CLIENT); - - // mix audio to be sent - VOIP_QUEUE_DECL(buffer_to_play, buffer_to_play_data); - { - for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) - { - opus_int16 to_send_to_cur[VOIP_EXPECTED_FRAME_COUNT] = { 0 }; // mix what other players said into this buffer - CONNECTED_PEERS(enet_host, other_player) - { - if (other_player != cur) - { - int other_player_index = (int)(int64_t)other_player->data; - Entity* other_player_entity = get_entity(&gs, gs.players[other_player_index].entity); - if (other_player_entity != NULL) - { - float dist = V2dist(entity_pos(this_player_entity), entity_pos(other_player_entity)); - float volume = lerp(1.0f, 0.0f, clamp01(dist / VOIP_DISTANCE_WHEN_CANT_HEAR)); - if (volume > 0.01f) - { - for (int frame_i = 0; frame_i < VOIP_EXPECTED_FRAME_COUNT; frame_i++) - { - to_send_to_cur[frame_i] += (opus_int16)((float)decoded_audio_packets[other_player_index][packet_i][frame_i] * volume); - } - } - } - } - } - OpusPacket* this_packet = (OpusPacket*)queue_push_element(&buffer_to_play); - opus_int32 ret = opus_encode(player_encoders[this_player_index], to_send_to_cur, VOIP_EXPECTED_FRAME_COUNT, this_packet->data, VOIP_PACKET_MAX_SIZE); - if (ret < 0) - { - Log("Failed to encode audio packet for player %d: opus error code %d\n", this_player_index, ret); - } - else { - this_packet->length = ret; - } - } - } - ServerToClient to_send = (ServerToClient){ - .cur_gs = &gs, - .your_player = this_player_index, - .playback_buffer = &buffer_to_play, - }; - - size_t len = 0; - if (server_to_client_serialize(&to_send, bytes_buffer, &len, MAX_SERVER_TO_CLIENT, this_player_entity, false)) - { - if (len > MAX_SERVER_TO_CLIENT - 8) - { - Log("Too much data quitting!\n"); - exit(-1); - } - - size_t compressed_len = 0; - lzo1x_1_compress(bytes_buffer, len, compressed_buffer, &compressed_len, (void*)lzo_working_mem); + if (num_audio_packets > MAX_AUDIO_PACKETS_TO_SEND) + { + Log("Wants %d, this is too many packets. Greater than the maximum %d\n", num_audio_packets, MAX_AUDIO_PACKETS_TO_SEND); + num_audio_packets = MAX_AUDIO_PACKETS_TO_SEND; + } + + opus_int16 decoded_audio_packets[MAX_PLAYERS][MAX_AUDIO_PACKETS_TO_SEND][VOIP_EXPECTED_FRAME_COUNT] = {0}; + + audio_time_to_send -= num_audio_packets * VOIP_TIME_PER_PACKET; + + // decode what everybody said + CONNECTED_PEERS(enet_host, cur) + { + int this_player_index = (int)(int64_t)cur->data; + for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) + { + opus_int16 *to_dump_to = decoded_audio_packets[this_player_index][packet_i]; + OpusPacket *cur_packet = (OpusPacket *)queue_pop_element(&player_voip_buffers[this_player_index]); + if (cur_packet == NULL) + opus_decode(player_decoders[this_player_index], NULL, 0, to_dump_to, VOIP_EXPECTED_FRAME_COUNT, 0); + else + opus_decode(player_decoders[this_player_index], cur_packet->data, cur_packet->length, to_dump_to, VOIP_EXPECTED_FRAME_COUNT, 0); + } + } + + // send gamestate to each player + CONNECTED_PEERS(enet_host, cur) + { + int this_player_index = (int)(int64_t)cur->data; + Entity *this_player_entity = get_entity(&gs, gs.players[this_player_index].entity); + if (this_player_entity == NULL) + continue; + // @Speed don't recreate the packet for every peer, gets expensive copying gamestate over and over again + char *bytes_buffer = malloc(sizeof *bytes_buffer * MAX_SERVER_TO_CLIENT); + char *compressed_buffer = malloc(sizeof *compressed_buffer * MAX_SERVER_TO_CLIENT); + + // mix audio to be sent + VOIP_QUEUE_DECL(buffer_to_play, buffer_to_play_data); + { + for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) + { + opus_int16 to_send_to_cur[VOIP_EXPECTED_FRAME_COUNT] = {0}; // mix what other players said into this buffer + CONNECTED_PEERS(enet_host, other_player) + { + if (other_player != cur) + { + int other_player_index = (int)(int64_t)other_player->data; + Entity *other_player_entity = get_entity(&gs, gs.players[other_player_index].entity); + if (other_player_entity != NULL) + { + float dist = V2dist(entity_pos(this_player_entity), entity_pos(other_player_entity)); + float volume = lerp(1.0f, 0.0f, clamp01(dist / VOIP_DISTANCE_WHEN_CANT_HEAR)); + if (volume > 0.01f) + { + for (int frame_i = 0; frame_i < VOIP_EXPECTED_FRAME_COUNT; frame_i++) + { + to_send_to_cur[frame_i] += (opus_int16)((float)decoded_audio_packets[other_player_index][packet_i][frame_i] * volume); + } + } + } + } + } + OpusPacket *this_packet = (OpusPacket *)queue_push_element(&buffer_to_play); + opus_int32 ret = opus_encode(player_encoders[this_player_index], to_send_to_cur, VOIP_EXPECTED_FRAME_COUNT, this_packet->data, VOIP_PACKET_MAX_SIZE); + if (ret < 0) + { + Log("Failed to encode audio packet for player %d: opus error code %d\n", this_player_index, ret); + } + else + { + this_packet->length = ret; + } + } + } + ServerToClient to_send = (ServerToClient){ + .cur_gs = &gs, + .your_player = this_player_index, + .audio_playback_buffer = &buffer_to_play, + }; + + size_t len = 0; + if (server_to_client_serialize(&to_send, bytes_buffer, &len, MAX_SERVER_TO_CLIENT, this_player_entity, false)) + { + if (len > MAX_SERVER_TO_CLIENT - 8) + { + Log("Too much data quitting!\n"); + exit(-1); + } + + size_t compressed_len = 0; + lzo1x_1_compress(bytes_buffer, len, compressed_buffer, &compressed_len, (void *)lzo_working_mem); #ifdef LOG_GAMESTATE_SIZE - Log("Size of gamestate packet before comrpession: %zu | After: %zu\n", len, compressed_len); + Log("Size of gamestate packet before comrpession: %zu | After: %zu\n", len, compressed_len); #endif - ENetPacket* gamestate_packet = enet_packet_create((void*)compressed_buffer, compressed_len, ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT); - int err = enet_peer_send(cur, 0, gamestate_packet); - if (err < 0) - { - Log("Enet failed to send packet error %d\n", err); - enet_packet_destroy(gamestate_packet); - } - } - else - { - Log("Failed to serialize data for client %d\n", this_player_index); - } - free(bytes_buffer); - free(compressed_buffer); - } - } - - } - } - for (int i = 0; i < MAX_PLAYERS; i++) - { - if (player_encoders[i] != NULL) - opus_encoder_destroy(player_encoders[i]); - if (player_decoders[i] != NULL) - opus_decoder_destroy(player_decoders[i]); - } - for (int i = 0; i < MAX_PLAYERS; i++) free(player_voip_buffers[i].data); - free(world_save_buffer); - destroy(&gs); - free(entity_data); - enet_host_destroy(enet_host); - enet_deinitialize(); - - printf("Cleanup\n"); + ENetPacket *gamestate_packet = enet_packet_create((void *)compressed_buffer, compressed_len, ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT); + int err = enet_peer_send(cur, 0, gamestate_packet); + if (err < 0) + { + Log("Enet failed to send packet error %d\n", err); + enet_packet_destroy(gamestate_packet); + } + } + else + { + Log("Failed to serialize data for client %d\n", this_player_index); + } + free(bytes_buffer); + free(compressed_buffer); + } + } + } + } + for (int i = 0; i < MAX_PLAYERS; i++) + { + if (player_encoders[i] != NULL) + opus_encoder_destroy(player_encoders[i]); + if (player_decoders[i] != NULL) + opus_decoder_destroy(player_decoders[i]); + } + for (int i = 0; i < MAX_PLAYERS; i++) + free(player_voip_buffers[i].data); + for (int i = 0; i < MAX_PLAYERS; i++) + free(player_input_queues[i].data); + free(world_save_buffer); + destroy(&gs); + free(entity_data); + enet_host_destroy(enet_host); + enet_deinitialize(); + + printf("Cleanup\n"); #ifdef PROFILING - SpallBufferQuit(&spall_ctx, &spall_buffer); - SpallQuit(&spall_ctx); + SpallBufferQuit(&spall_ctx, &spall_buffer); + SpallQuit(&spall_ctx); #endif } \ No newline at end of file diff --git a/types.h b/types.h index e7e54ad..ecfaec2 100644 --- a/types.h +++ b/types.h @@ -1,7 +1,9 @@ #pragma once +#include "ipsettings.h" + #define MAX_PLAYERS 16 -#define MAX_ENTITIES 1024*25 +#define MAX_ENTITIES 1024 * 25 #define BOX_SIZE 0.25f #define PLAYER_SIZE ((V2){.x = BOX_SIZE, .y = BOX_SIZE}) #define PLAYER_MASS 0.5f @@ -17,14 +19,18 @@ #define THRUSTER_ENERGY_USED_PER_SECOND 0.005f #define VISION_RADIUS 12.0f #define MAX_SERVER_TO_CLIENT 1024 * 512 // maximum size of serialized gamestate buffer -#define MAX_CLIENT_TO_SERVER 1024 * 10 // maximum size of serialized inputs and mic data +#define MAX_CLIENT_TO_SERVER 1024 * 10 // maximum size of serialized inputs and mic data #define SUN_RADIUS 10.0f #define INSTANT_DEATH_DISTANCE_FROM_SUN 2000.0f -#define SUN_POS ((V2){50.0f,0.0f}) +#define SUN_POS ((V2){50.0f, 0.0f}) +#ifdef NO_GRAVITY +#define SUN_GRAVITY_STRENGTH 0.1f +#else #define SUN_GRAVITY_STRENGTH (9.0e2f) +#endif #define SOLAR_ENERGY_PER_SECOND 0.02f #define DAMAGE_TO_PLAYER_PER_BLOCK 0.1f -#define BATTERY_CAPACITY DAMAGE_TO_PLAYER_PER_BLOCK*0.7f +#define BATTERY_CAPACITY DAMAGE_TO_PLAYER_PER_BLOCK * 0.7f #define PLAYER_ENERGY_RECHARGE_PER_SECOND 0.1f #define EXPLOSION_TIME 0.5f #define EXPLOSION_PUSH_STRENGTH 5.0f @@ -35,18 +41,27 @@ #define TIME_BETWEEN_WORLD_SAVE 30.0f // VOIP -#define VOIP_PACKET_BUFFER_SIZE 15 // audio. Must be bigger than 2 +#define VOIP_PACKET_BUFFER_SIZE 15 // audio. Must be bigger than 2 #define VOIP_EXPECTED_FRAME_COUNT 480 #define VOIP_SAMPLE_RATE 48000 -#define VOIP_TIME_PER_PACKET 1.0f / ((float)(VOIP_SAMPLE_RATE/VOIP_EXPECTED_FRAME_COUNT)) // in seconds +#define VOIP_TIME_PER_PACKET (1.0f / ((float)(VOIP_SAMPLE_RATE / VOIP_EXPECTED_FRAME_COUNT))) // in seconds #define VOIP_PACKET_MAX_SIZE 4000 -#define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS*0.8f) +#define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS * 0.8f) +#define MAX_REPREDICTION_TIME (TIMESTEP * 50.0f) #define TIME_BETWEEN_SEND_GAMESTATE (1.0f / 20.0f) #define TIME_BETWEEN_INPUT_PACKETS (1.0f / 20.0f) #define TIMESTEP (1.0f / 60.0f) // server required to simulate at this, defines what tick the game is on #define SERVER_PORT 2551 -#define INPUT_BUFFER 6 +#define LOCAL_INPUT_QUEUE_MAX 90 // please god let you not have more than 90 frames of game latency +#define INPUT_QUEUE_MAX 15 + +// cross platform threadlocal variables +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#define THREADLOCAL __declspec(thread) +#else +#define THREADLOCAL __thread +#endif // must make this header and set the target address, just #define SERVER_ADDRESS "127.0.0.1" #include "ipsettings.h" // don't leak IP! @@ -72,7 +87,7 @@ void sgp_set_color(float, float, float, float); // somehow automatically or easily cast to floats typedef struct sgp_vec2 { - float x, y; + float x, y; } sgp_vec2; typedef sgp_vec2 sgp_point; @@ -85,8 +100,8 @@ typedef void cpBody; typedef void cpShape; #endif -#include #include "queue.h" +#include #ifndef OPUS_TYPES_H typedef int opus_int32; @@ -103,509 +118,512 @@ typedef int opus_int32; typedef sgp_vec2 V2; typedef sgp_point P2; -#define Log(...){ \ +#define Log(...) \ + { \ fprintf(stdout, "%s:%d | ", __FILE__, __LINE__); \ - fprintf(stdout, __VA_ARGS__);} + fprintf(stdout, __VA_ARGS__); \ + } enum BoxType { - BoxHullpiece, - BoxThruster, - BoxBattery, - BoxCockpit, - BoxMedbay, - BoxSolarPanel, - BoxExplosive, - BoxLast, + BoxHullpiece, + BoxThruster, + BoxBattery, + BoxCockpit, + BoxMedbay, + BoxSolarPanel, + BoxExplosive, + BoxLast, }; enum CompassRotation { - Right, - Down, - Left, - Up, - RotationLast, + Right, + Down, + Left, + Up, + RotationLast, }; enum Squad { - SquadNone, - SquadRed, - SquadGreen, - SquadBlue, - SquadPurple, - SquadLast, + SquadNone, + SquadRed, + SquadGreen, + SquadBlue, + SquadPurple, + SquadLast, }; // when generation is 0, invalid ID typedef struct EntityID { - unsigned int generation; // VERY IMPORTANT if 0 then EntityID points to nothing, generation >= 1 - unsigned int index; // index into the entity arena + unsigned int generation; // VERY IMPORTANT if 0 then EntityID points to nothing, generation >= 1 + unsigned int index; // index into the entity arena } EntityID; static bool entityids_same(EntityID a, EntityID b) { - return (a.generation == b.generation) && (a.index == b.index); + return (a.generation == b.generation) && (a.index == b.index); } // when updated, must update serialization, comparison in main.c, and the server // on input received processing function typedef struct InputFrame { - uint64_t tick; - size_t id; // each input has unique, incrementing, I.D, so server doesn't double process inputs. Inputs to server should be ordered from 0-max like biggest id-smallest. This is done so if packet loss server still processes input - V2 movement; + uint64_t tick; + V2 movement; - int take_over_squad; // -1 means not taking over any squad - bool accept_cur_squad_invite; - bool reject_cur_squad_invite; - EntityID invite_this_player; // null means inviting nobody! @Robust make it so just sends interact pos input, and server processes who to invite. This depends on client side prediction + proper input processing at the right tick. + int take_over_squad; // -1 means not taking over any squad + bool accept_cur_squad_invite; + bool reject_cur_squad_invite; + EntityID invite_this_player; // null means inviting nobody! @Robust make it so just sends interact pos input, and server processes who to invite. This depends on client side prediction + proper input processing at the right tick. - bool seat_action; - EntityID seat_to_inhabit; - V2 hand_pos; // local to player transationally but not rotationally unless field below is not null, then it's local to that grid - // @BeforeShip bounds check on the hand_pos so that players can't reach across the entire map - EntityID grid_hand_pos_local_to; // when not null, hand_pos is local to this grid. this prevents bug where at high speeds the built block is in the wrong place on the selected grid + bool seat_action; + V2 hand_pos; // local to player transationally but not rotationally + // @BeforeShip bounds check on the hand_pos so that players can't reach across the entire map - bool dobuild; - enum BoxType build_type; - enum CompassRotation build_rotation; + bool dobuild; + enum BoxType build_type; + enum CompassRotation build_rotation; } InputFrame; typedef struct Entity { - bool exists; - EntityID next_free_entity; - unsigned int generation; - - bool no_save_to_disk; // stuff generated later on, like player's bodies or space stations that respawn. - - float damage; // used by box and player - cpBody* body; // used by grid, player, and box - cpShape* shape; // must be a box so shape_size can be set appropriately, and serialized - - // for serializing the shape - // @Robust remove shape_parent_entity from this struct, use the shape's body to figure out - // what the shape's parent entity is - EntityID shape_parent_entity; // can't be zero if shape is nonzero - V2 shape_size; - - // player - bool is_player; - enum Squad presenting_squad; - EntityID currently_inside_of_box; - enum Squad squad_invited_to; // if squad none, then no squad invite - float goldness; // how much the player is a winner - - // explosion - bool is_explosion; - V2 explosion_pos; - V2 explosion_vel; - float explosion_progresss; // in seconds - - // grids - bool is_grid; - float total_energy_capacity; - EntityID boxes; - - // boxes - bool is_box; - bool always_visible; // always serialized to the player - enum BoxType box_type; - bool is_explosion_unlock; - EntityID next_box; - EntityID prev_box; // doubly linked so can remove in middle of chain - enum CompassRotation compass_rotation; - bool indestructible; - float wanted_thrust; // the thrust command applied to the thruster - float thrust; // the actual thrust it can provide based on energy sources in the grid - float energy_used; // battery - float sun_amount; // solar panel, between 0 and 1 - EntityID player_who_is_inside_of_me; + bool exists; + EntityID next_free_entity; + unsigned int generation; + + bool no_save_to_disk; // stuff generated later on, like player's bodies or space stations that respawn. + + float damage; // used by box and player + cpBody *body; // used by grid, player, and box + cpShape *shape; // must be a box so shape_size can be set appropriately, and serialized + + // for serializing the shape + // @Robust remove shape_parent_entity from this struct, use the shape's body to figure out + // what the shape's parent entity is + EntityID shape_parent_entity; // can't be zero if shape is nonzero + V2 shape_size; + + // player + bool is_player; + enum Squad presenting_squad; + EntityID currently_inside_of_box; + enum Squad squad_invited_to; // if squad none, then no squad invite + float goldness; // how much the player is a winner + + // explosion + bool is_explosion; + V2 explosion_pos; + V2 explosion_vel; + float explosion_progresss; // in seconds + + // grids + bool is_grid; + float total_energy_capacity; + EntityID boxes; + + // boxes + bool is_box; + bool always_visible; // always serialized to the player + enum BoxType box_type; + bool is_explosion_unlock; + EntityID next_box; + EntityID prev_box; // doubly linked so can remove in middle of chain + enum CompassRotation compass_rotation; + bool indestructible; + float wanted_thrust; // the thrust command applied to the thruster + float thrust; // the actual thrust it can provide based on energy sources in the grid + float energy_used; // battery + float sun_amount; // solar panel, between 0 and 1 + EntityID player_who_is_inside_of_me; } Entity; typedef struct Player { - bool connected; - bool unlocked_bombs; - enum Squad squad; - EntityID entity; - EntityID last_used_medbay; - InputFrame input; + bool connected; + bool unlocked_bombs; + enum Squad squad; + EntityID entity; + EntityID last_used_medbay; + InputFrame input; } Player; // gotta update the serialization functions when this changes typedef struct GameState { - cpSpace* space; + cpSpace *space; - double time; + double time; // @Robust separate tick integer not prone to precision issues - V2 goldpos; + V2 goldpos; - Player players[MAX_PLAYERS]; + Player players[MAX_PLAYERS]; - EntityID cur_spacestation; + EntityID cur_spacestation; - // Entity arena - // ent:ity pointers can't move around because of how the physics engine handles user data. - // if you really need this, potentially refactor to store entity IDs instead of pointers - // in the shapes and bodies of chipmunk. Would require editing the library I think - Entity* entities; - unsigned int max_entities; // maximum number of entities possible in the entities list - unsigned int cur_next_entity; // next entity to pass on request of a new entity if the free list is empty - EntityID free_list; + // Entity arena + // ent:ity pointers can't move around because of how the physics engine handles user data. + // if you really need this, potentially refactor to store entity IDs instead of pointers + // in the shapes and bodies of chipmunk. Would require editing the library I think + Entity *entities; + unsigned int max_entities; // maximum number of entities possible in the entities list + unsigned int cur_next_entity; // next entity to pass on request of a new entity if the free list is empty + EntityID free_list; } GameState; -#define PLAYERS_ITER(players, cur) for(Player * cur = players; cur < players+MAX_PLAYERS; cur++) if(cur->connected) +#define PLAYERS_ITER(players, cur) \ + for (Player *cur = players; cur < players + MAX_PLAYERS; cur++) \ + if (cur->connected) #define PI 3.14159f -#define TAU (PI*2.0f) +#define TAU (PI * 2.0f) // returns in radians static float rotangle(enum CompassRotation rot) { - switch (rot) - { - case Right: - return 0.0f; - break; - case Down: - return -PI / 2.0f; - break; - case Left: - return -PI; - break; - case Up: - return -3.0f * PI / 2.0f; - break; - default: - Log("Unknown rotation %d\n", rot); - return -0.0f; - break; - } -} - -typedef struct OpusPacket { - opus_int32 length; - char data[VOIP_PACKET_MAX_SIZE]; + switch (rot) + { + case Right: + return 0.0f; + break; + case Down: + return -PI / 2.0f; + break; + case Left: + return -PI; + break; + case Up: + return -3.0f * PI / 2.0f; + break; + default: + Log("Unknown rotation %d\n", rot); + return -0.0f; + break; + } +} + +typedef struct OpusPacket +{ + opus_int32 length; + char data[VOIP_PACKET_MAX_SIZE]; } OpusPacket; typedef struct ServerToClient { - struct GameState* cur_gs; - Queue* playback_buffer; - int your_player; + struct GameState *cur_gs; + Queue *audio_playback_buffer; + int your_player; } ServerToClient; typedef struct ClientToServer { - Queue* mic_data; // on serialize, flushes this of packets. On deserialize, fills it - InputFrame inputs[INPUT_BUFFER]; + Queue *mic_data; // on serialize, flushes this of packets. On deserialize, fills it + Queue *input_data; // does not flush on serialize! must be in order of tick } ClientToServer; #define DeferLoop(start, end) \ - for (int _i_ = ((start), 0); _i_ == 0; _i_ += 1, (end)) + for (int _i_ = ((start), 0); _i_ == 0; _i_ += 1, (end)) // server -void server(void* info); // data parameter required from thread api... +void server(void *info); // data parameter required from thread api... // gamestate -EntityID create_spacestation(GameState* gs); -void initialize(struct GameState* gs, void* entity_arena, size_t entity_arena_size); -void destroy(struct GameState* gs); -void process(struct GameState* gs, float dt); // does in place -Entity* closest_to_point_in_radius(struct GameState* gs, V2 point, float radius); -uint64_t tick(struct GameState* gs); +EntityID create_spacestation(GameState *gs); +void initialize(struct GameState *gs, void *entity_arena, size_t entity_arena_size); +void destroy(struct GameState *gs); +void process_fixed_timestep(GameState *gs); +void process(struct GameState *gs, float dt); // does in place +Entity *closest_to_point_in_radius(struct GameState *gs, V2 point, float radius); +uint64_t tick(struct GameState *gs); // all of these return if successful or not -bool server_to_client_serialize(struct ServerToClient* msg, char* bytes, size_t* out_len, size_t max_len, Entity* for_this_player, bool to_disk); -bool server_to_client_deserialize(struct ServerToClient* msg, char* bytes, size_t max_len, bool from_disk); -bool client_to_server_deserialize(GameState* gs, struct ClientToServer* msg, char* bytes, size_t max_len); -bool client_to_server_serialize(GameState* gs, struct ClientToServer* msg, char* bytes, size_t* out_len, size_t max_len); +bool server_to_client_serialize(struct ServerToClient *msg, char *bytes, size_t *out_len, size_t max_len, Entity *for_this_player, bool to_disk); +bool server_to_client_deserialize(struct ServerToClient *msg, char *bytes, size_t max_len, bool from_disk); +bool client_to_server_deserialize(GameState *gs, struct ClientToServer *msg, char *bytes, size_t max_len); +bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, char *bytes, size_t *out_len, size_t max_len); // entities -Entity* get_entity(struct GameState* gs, EntityID id); -Entity* new_entity(struct GameState* gs); -EntityID get_id(struct GameState* gs, Entity* e); -V2 entity_pos(Entity* e); -void entity_set_rotation(Entity* e, float rot); -void entity_set_pos(Entity* e, V2 pos); -float entity_rotation(Entity* e); -void entity_ensure_in_orbit(Entity* e); -void entity_destroy(GameState* gs, Entity* e); +Entity *get_entity(struct GameState *gs, EntityID id); +Entity *new_entity(struct GameState *gs); +EntityID get_id(struct GameState *gs, Entity *e); +V2 entity_pos(Entity *e); +void entity_set_rotation(Entity *e, float rot); +void entity_set_pos(Entity *e, V2 pos); +float entity_rotation(Entity *e); +void entity_ensure_in_orbit(Entity *e); +void entity_destroy(GameState *gs, Entity *e); #define BOX_CHAIN_ITER(gs, cur, starting_box) for (Entity *cur = get_entity(gs, starting_box); cur != NULL; cur = get_entity(gs, cur->next_box)) #define BOXES_ITER(gs, cur, grid_entity_ptr) BOX_CHAIN_ITER(gs, cur, (grid_entity_ptr)->boxes) // grid -void grid_create(struct GameState* gs, Entity* e); -void box_create(struct GameState* gs, Entity* new_box, Entity* grid, V2 pos); -V2 grid_com(Entity* grid); -V2 grid_vel(Entity* grid); -V2 box_vel(Entity* box); -V2 grid_local_to_world(Entity* grid, V2 local); -V2 grid_world_to_local(Entity* grid, V2 world); -V2 grid_snapped_box_pos(Entity* grid, V2 world); // returns the snapped pos in world coords -float entity_angular_velocity(Entity* grid); -V2 entity_shape_pos(Entity* box); -float box_rotation(Entity* box); +void grid_create(struct GameState *gs, Entity *e); +void box_create(struct GameState *gs, Entity *new_box, Entity *grid, V2 pos); +V2 grid_com(Entity *grid); +V2 grid_vel(Entity *grid); +V2 box_vel(Entity *box); +V2 grid_local_to_world(Entity *grid, V2 local); +V2 grid_world_to_local(Entity *grid, V2 world); +V2 grid_snapped_box_pos(Entity *grid, V2 world); // returns the snapped pos in world coords +float entity_angular_velocity(Entity *grid); +V2 entity_shape_pos(Entity *box); +float box_rotation(Entity *box); // thruster -V2 box_facing_vector(Entity* box); -V2 thruster_force(Entity* box); +V2 box_facing_vector(Entity *box); +V2 thruster_force(Entity *box); // debug draw void dbg_drawall(); void dbg_line(V2 from, V2 to); void dbg_rect(V2 center); -typedef struct ServerThreadInfo { - ma_mutex info_mutex; - const char* world_save; - bool should_quit; +typedef struct ServerThreadInfo +{ + ma_mutex info_mutex; + const char *world_save; + bool should_quit; } ServerThreadInfo; // all the math is static so that it can be defined in each compilation unit its included in typedef struct AABB { - float x, y, width, height; + float x, y, width, height; } AABB; static AABB centered_at(V2 point, V2 size) { - return (AABB) - { - .x = point.x - size.x / 2.0f, - .y = point.y - size.y / 2.0f, - .width = size.x, - .height = size.y, - }; + return (AABB){ + .x = point.x - size.x / 2.0f, + .y = point.y - size.y / 2.0f, + .width = size.x, + .height = size.y, + }; } static bool has_point(AABB aabb, V2 point) { - return point.x > aabb.x && point.x < aabb.x + aabb.width && point.y > aabb.y && point.y < aabb.y + aabb.height; + return point.x > aabb.x && point.x < aabb.x + aabb.width && point.y > aabb.y && point.y < aabb.y + aabb.height; } static V2 V2add(V2 a, V2 b) { - return (V2) { - .x = a.x + b.x, - .y = a.y + b.y, - }; + return (V2){ + .x = a.x + b.x, + .y = a.y + b.y, + }; } static V2 V2scale(V2 a, float f) { - return (V2) { - .x = a.x * f, - .y = a.y * f, - }; + return (V2){ + .x = a.x * f, + .y = a.y * f, + }; } static float V2lengthsqr(V2 v) { - return v.x * v.x + v.y * v.y; + return v.x * v.x + v.y * v.y; } static float V2length(V2 v) { - return sqrtf(V2lengthsqr(v)); + return sqrtf(V2lengthsqr(v)); } static V2 V2normalize(V2 v) { - return V2scale(v, 1.0f / V2length(v)); + return V2scale(v, 1.0f / V2length(v)); } - static float V2dot(V2 a, V2 b) { - return a.x * b.x + a.y * b.y; + return a.x * b.x + a.y * b.y; } static float V2projectvalue(V2 vec, V2 onto) { - float length_onto = V2length(onto); - return V2dot(vec, onto) / (length_onto * length_onto); + float length_onto = V2length(onto); + return V2dot(vec, onto) / (length_onto * length_onto); } static V2 V2project(V2 vec, V2 onto) { - return V2scale(onto, V2projectvalue(vec, onto)); + return V2scale(onto, V2projectvalue(vec, onto)); } static V2 V2rotate(V2 vec, float theta) { - return (V2) { - .x = vec.x * cosf(theta) - vec.y * sinf(theta), - .y = vec.x * sinf(theta) + vec.y * cosf(theta), - }; + return (V2){ + .x = vec.x * cosf(theta) - vec.y * sinf(theta), + .y = vec.x * sinf(theta) + vec.y * cosf(theta), + }; } // also known as atan2 static float V2angle(V2 vec) { - return atan2f(vec.y, vec.x); + return atan2f(vec.y, vec.x); } static V2 V2sub(V2 a, V2 b) { - return (V2) { - .x = a.x - b.x, - .y = a.y - b.y, - }; + return (V2){ + .x = a.x - b.x, + .y = a.y - b.y, + }; } static bool V2equal(V2 a, V2 b, float eps) { - return V2length(V2sub(a, b)) < eps; + return V2length(V2sub(a, b)) < eps; } static inline float clamp01(float f) { - return fmaxf(0.0f, fminf(f, 1.0f)); + return fmaxf(0.0f, fminf(f, 1.0f)); } static float V2distsqr(V2 from, V2 to) { - return V2lengthsqr(V2sub(to, from)); + return V2lengthsqr(V2sub(to, from)); } static float V2dist(V2 from, V2 to) { - return sqrtf(V2distsqr(from, to)); + return sqrtf(V2distsqr(from, to)); } static inline float clamp(float f, float minimum, float maximum) { - if (f < minimum) - return minimum; - if (f > maximum) - return maximum; - return f; + if (f < minimum) + return minimum; + if (f > maximum) + return maximum; + return f; } static float fract(float f) { - return f - floorf(f); + return f - floorf(f); } static float lerp(float a, float b, float f) { - return a * (1.0f - f) + (b * f); + return a * (1.0f - f) + (b * f); } -static float lerp_angle(float p_from, float p_to, float p_weight) { - float difference = fmodf(p_to - p_from, (float)TAU); - float distance = fmodf(2.0f * difference, (float)TAU) - difference; - return p_from + distance * p_weight; +static float lerp_angle(float p_from, float p_to, float p_weight) +{ + float difference = fmodf(p_to - p_from, (float)TAU); + float distance = fmodf(2.0f * difference, (float)TAU) - difference; + return p_from + distance * p_weight; } static V2 V2floor(V2 p) { - return (V2) { floorf(p.x), floorf(p.y) }; + return (V2){floorf(p.x), floorf(p.y)}; } static V2 V2fract(V2 p) { - return (V2) { fract(p.x), fract(p.y) }; + return (V2){fract(p.x), fract(p.y)}; } /* float noise(V2 p) { - V2 id = V2floor(p); - V2 f = V2fract(p); + V2 id = V2floor(p); + V2 f = V2fract(p); - V2 u = V2dot(f, f) * (3.0f - 2.0f * f); + V2 u = V2dot(f, f) * (3.0f - 2.0f * f); - return mix(mix(random(id + V2(0.0, 0.0)), - random(id + V2(1.0, 0.0)), u.x), - mix(random(id + V2(0.0, 1.0)), - random(id + V2(1.0, 1.0)), u.x), - u.y); + return mix(mix(random(id + V2(0.0, 0.0)), + random(id + V2(1.0, 0.0)), u.x), + mix(random(id + V2(0.0, 1.0)), + random(id + V2(1.0, 1.0)), u.x), + u.y); } float fbm(V2 p) { - float f = 0.0; - float gat = 0.0; + float f = 0.0; + float gat = 0.0; - for (float octave = 0.; octave < 5.; ++octave) - { - float la = pow(2.0, octave); - float ga = pow(0.5, octave + 1.); - f += ga * noise(la * p); - gat += ga; - } + for (float octave = 0.; octave < 5.; ++octave) + { + float la = pow(2.0, octave); + float ga = pow(0.5, octave + 1.); + f += ga * noise(la * p); + gat += ga; + } - f = f / gat; + f = f / gat; - return f; + return f; } */ static V2 V2lerp(V2 a, V2 b, float factor) { - V2 to_return = { 0 }; - to_return.x = lerp(a.x, b.x, factor); - to_return.y = lerp(a.y, b.y, factor); + V2 to_return = {0}; + to_return.x = lerp(a.x, b.x, factor); + to_return.y = lerp(a.y, b.y, factor); - return to_return; + return to_return; } // for random generation static float hash11(float p) { - p = fract(p * .1031f); - p *= p + 33.33f; - p *= p + p; - return fract(p); + p = fract(p * .1031f); + p *= p + 33.33f; + p *= p + p; + return fract(p); } typedef struct Color { - float r, g, b, a; + float r, g, b, a; } Color; static Color colhex(int r, int g, int b) { - return (Color) { - .r = (float)r / 255.0f, - .g = (float)g / 255.0f, - .b = (float)b / 255.0f, - .a = 1.0f, - }; + return (Color){ + .r = (float)r / 255.0f, + .g = (float)g / 255.0f, + .b = (float)b / 255.0f, + .a = 1.0f, + }; } static Color colhexcode(int hexcode) { - // 0x020509; - int r = (hexcode >> 16) & 0xFF; - int g = (hexcode >> 8) & 0xFF; - int b = (hexcode >> 0) & 0xFF; - return colhex(r, g, b); + // 0x020509; + int r = (hexcode >> 16) & 0xFF; + int g = (hexcode >> 8) & 0xFF; + int b = (hexcode >> 0) & 0xFF; + return colhex(r, g, b); } static Color Collerp(Color a, Color b, float factor) { - Color to_return = { 0 }; - to_return.r = lerp(a.r, b.r, factor); - to_return.g = lerp(a.g, b.g, factor); - to_return.b = lerp(a.b, b.b, factor); - to_return.a = lerp(a.a, b.a, factor); + Color to_return = {0}; + to_return.r = lerp(a.r, b.r, factor); + to_return.g = lerp(a.g, b.g, factor); + to_return.b = lerp(a.b, b.b, factor); + to_return.a = lerp(a.a, b.a, factor); - return to_return; + return to_return; } static void set_color(Color c) { - sgp_set_color(c.r, c.g, c.b, c.a); + sgp_set_color(c.r, c.g, c.b, c.a); } #define WHITE \ - (Color) { .r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f } + (Color) { .r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f } #define RED \ - (Color) { .r = 1.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f } + (Color) { .r = 1.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f } #define GOLD colhex(255, 215, 0)