diff --git a/.gitignore b/.gitignore index e6a0f40..9fd6d7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# profiling results +*.spall compile_commands.json .cache/ enc_temp_folder/ diff --git a/Flight.vcxproj b/Flight.vcxproj index 8645300..539277f 100644 --- a/Flight.vcxproj +++ b/Flight.vcxproj @@ -58,6 +58,7 @@ true v143 Unicode + false Application @@ -140,7 +141,7 @@ Level3 true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions);DEBUG_RENDERING + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);DEBUG_RENDERING;SERVER_ADDRESS="127.0.0.1" true $(ProjectDir)\thirdparty\enet\include;$(ProjectDir)\thirdparty\Chipmunk2D\include\chipmunk;$(ProjectDir)\thirdparty;$(ProjectDir)\thirdparty\Chipmunk2D\include;$(ProjectDir)\thirdparty\minilzo;$(ProjectDir)\thirdparty\opus\include true diff --git a/gamestate.c b/gamestate.c index 3c39378..b575c9b 100644 --- a/gamestate.c +++ b/gamestate.c @@ -3,6 +3,8 @@ #include "queue.h" #include "stdbool.h" #include "types.h" +#define PROFILING_IMPL +#include "profiling.h" #include "ipsettings.h" // debug/developer settings @@ -73,7 +75,7 @@ bool cloaking_active(GameState *gs, Entity *e) { // cloaking doesn't work for first 1/2 second of game because when initializing // everything needs to be uncloaked - return gs->time >= 0.5 && (gs->time - e->time_was_last_cloaked) <= TIMESTEP * 2.0; + return time(gs) >= 0.5 && (time(gs) - e->time_was_last_cloaked) <= TIMESTEP * 2.0; } bool is_cloaked(GameState *gs, Entity *e, Entity *this_players_perspective) @@ -391,7 +393,6 @@ void grid_create(GameState *gs, Entity *e) create_body(gs, e); } - void entity_set_rotation(Entity *e, double rot) { assert(e->body != NULL); @@ -1339,7 +1340,9 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s) SER_ASSERT(cur_next_entity <= ser->max_entity_index); SER_VAR(&s->your_player); - SER_VAR(&gs->time); + + SER_VAR(&gs->tick); + SER_VAR(&gs->subframe_time); SER_MAYBE_RETURN(ser_V2(ser, &gs->goldpos)); @@ -1661,7 +1664,7 @@ static void cloaking_shield_callback_func(cpShape *shape, cpContactPointSet *poi GameState *gs = entitys_gamestate(from_cloaking_box); Entity *to_cloak = cp_shape_entity(shape); - to_cloak->time_was_last_cloaked = gs->time; + to_cloak->time_was_last_cloaked = time(gs); to_cloak->last_cloaked_by_squad = from_cloaking_box->owning_squad; } @@ -1801,7 +1804,13 @@ cpVect thruster_force(Entity *box) uint64_t tick(GameState *gs) { - return (uint64_t)floor(gs->time / ((double)TIMESTEP)); + // return (uint64_t)floor(gs->time / ((double)TIMESTEP)); + return gs->tick; +} + +double time(GameState *gs) +{ + return ((double)gs->tick * TIMESTEP) + gs->subframe_time; } Entity *grid_to_build_on(GameState *gs, cpVect world_hand_pos) @@ -2110,693 +2119,732 @@ void exit_seat(GameState *gs, Entity *seat_in, Entity *p) cpBodySetVelocity(p->body, cpBodyGetVelocity(box_grid(seat_in)->body)); } -void process_fixed_timestep(GameState *gs) +void process(struct GameState *gs, double dt, bool is_subframe) { - process(gs, TIMESTEP); -} - -void process(GameState *gs, double dt) -{ - assert(gs->space != NULL); - - gs->time += dt; - - // process sun gravity - SUNS_ITER(gs) + PROFILE_SCOPE("Gameplay processing") { - Entity *from_sun = i.sun; - cpVect accel = {0}; - SUNS_ITER(gs) - { - Entity *other_sun = i.sun; - if (other_sun != from_sun) - { - accel = cpvadd(accel, sun_gravity_accel_for_entity(from_sun, other_sun)); - } - } -#ifndef NO_GRAVITY - from_sun->sun_vel = cpvadd(from_sun->sun_vel, cpvmult(accel, dt)); - from_sun->sun_pos = cpvadd(from_sun->sun_pos, cpvmult(from_sun->sun_vel, dt)); + assert(gs->space != NULL); - if (cpvlength(from_sun->sun_pos) >= INSTANT_DEATH_DISTANCE_FROM_CENTER) + PROFILE_SCOPE("subframe stuff") { - from_sun->sun_vel = cpvmult(from_sun->sun_vel, -0.8); - from_sun->sun_pos = cpvmult(cpvnormalize(from_sun->sun_pos), INSTANT_DEATH_DISTANCE_FROM_CENTER); - } -#endif - } - // 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 + if (is_subframe) { - bool squad_taken = false; - PLAYERS_ITER(gs->players, other_player) + gs->subframe_time += dt; + while (gs->subframe_time > TIMESTEP) { - if (other_player->squad == player->input.take_over_squad) - { - squad_taken = true; - break; - } + gs->subframe_time -= TIMESTEP; + gs->tick++; } - 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); - // player respawning - if (p == NULL) - { - p = new_entity(gs); - create_player_entity(gs, p); - player->entity = get_id(gs, p); - Entity *medbay = get_entity(gs, player->last_used_medbay); - entity_ensure_in_orbit(gs, p); - if (medbay != NULL) - { - exit_seat(gs, medbay, p); - p->damage = 0.95; - } - } - assert(p->is_player); - p->owning_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) + else { - p->squad_invited_to = SquadNone; - player->input.reject_cur_squad_invite = false; + assert(gs->subframe_time == 0.0); + gs->tick++; } } -#ifdef INFINITE_RESOURCES - p->damage = 0.0; -#endif - // update gold win condition - if (cpvlength(cpvsub((cpBodyGetPosition(p->body)), gs->goldpos)) < GOLD_COLLECT_RADIUS) + PROFILE_SCOPE("sun gravity") { - p->goldness += 0.1; - p->damage = 0.0; - gs->goldpos = (cpVect){.x = hash11((float)gs->time) * 20.0, .y = hash11((float)gs->time - 13.6) * 20.0}; - } -#if 1 - cpVect 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 + SUNS_ITER(gs) { - cpPointQueryInfo query_info = {0}; - cpShape *result = cpSpacePointQueryNearest(gs->space, (world_hand_pos), 0.1, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &query_info); - if (result != NULL) + Entity *from_sun = i.sun; + cpVect accel = {0}; + SUNS_ITER(gs) { - Entity *potential_seat = cp_shape_entity(result); - assert(potential_seat->is_box); - - if (potential_seat->box_type == BoxScanner) // learn everything from the scanner + Entity *other_sun = i.sun; + if (other_sun != from_sun) { - player->box_unlocks |= potential_seat->blueprints_learned; - } - if (potential_seat->box_type == BoxMerge) // disconnect! - { - potential_seat->wants_disconnect = true; - grid_correct_for_holes(gs, box_grid(potential_seat)); - assert(potential_seat->exists); - assert(potential_seat->is_box); - assert(potential_seat->box_type == BoxMerge); - } - 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); - if (potential_seat->box_type == BoxMedbay) - player->last_used_medbay = p->currently_inside_of_box; - } + accel = cpvadd(accel, sun_gravity_accel_for_entity(from_sun, other_sun)); } } - else +#ifndef NO_GRAVITY + from_sun->sun_vel = cpvadd(from_sun->sun_vel, cpvmult(accel, dt)); + from_sun->sun_pos = cpvadd(from_sun->sun_pos, cpvmult(from_sun->sun_vel, dt)); + + if (cpvlength(from_sun->sun_pos) >= INSTANT_DEATH_DISTANCE_FROM_CENTER) { - Log("No seat to get into for a player at point %f %f\n", world_hand_pos.x, world_hand_pos.y); + from_sun->sun_vel = cpvmult(from_sun->sun_vel, -0.8); + from_sun->sun_pos = cpvmult(cpvnormalize(from_sun->sun_pos), INSTANT_DEATH_DISTANCE_FROM_CENTER); } - } - 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 } } -#endif - // process movement + PROFILE_SCOPE("input processing") { - // no cheating by making movement bigger than length 1 - cpVect movement_this_tick = (cpVect){0}; - double rotation_this_tick = 0.0; - if (cpvlength(player->input.movement) > 0.0) - { - movement_this_tick = cpvmult(cpvnormalize(player->input.movement), clamp(cpvlength(player->input.movement), 0.0, 1.0)); - player->input.movement = (cpVect){0}; - } - if (fabs(player->input.rotation) > 0.0) - { - rotation_this_tick = player->input.rotation; - if (rotation_this_tick > 1.0) - rotation_this_tick = 1.0; - if (rotation_this_tick < -1.0) - rotation_this_tick = -1.0; - player->input.rotation = 0.0; - } - Entity *seat_inside_of = get_entity(gs, p->currently_inside_of_box); - - // strange rare bug I saw happen, related to explosives, but no idea how to - // reproduce. @Robust put a breakpoint here, reproduce, and fix it! - if (seat_inside_of != NULL && !seat_inside_of->is_box) - { - Log("Strange thing happened where player was in non box seat!\n"); - seat_inside_of = NULL; - p->currently_inside_of_box = (EntityID){0}; - } - if (seat_inside_of == NULL) + PLAYERS_ITER(gs->players, player) { - cpShapeSetFilter(p->shape, PLAYER_SHAPE_FILTER); - cpBodyApplyForceAtWorldPoint(p->body, (cpvmult(movement_this_tick, PLAYER_JETPACK_FORCE)), cpBodyGetPosition(p->body)); - cpBodySetTorque(p->body, rotation_this_tick * PLAYER_JETPACK_TORQUE); - p->damage += cpvlength(movement_this_tick) * dt * PLAYER_JETPACK_SPICE_PER_SECOND; - p->damage += fabs(rotation_this_tick) * dt * PLAYER_JETPACK_ROTATION_ENERGY_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, (entity_pos(seat_inside_of))); - cpBodySetVelocity(p->body, (box_vel(seat_inside_of))); - - // share cloaking with box - p->time_was_last_cloaked = seat_inside_of->time_was_last_cloaked; - p->last_cloaked_by_squad = seat_inside_of->last_cloaked_by_squad; - - // set thruster thrust from movement - if (seat_inside_of->box_type == BoxCockpit) + if (player->input.take_over_squad >= 0) { - Entity *g = get_entity(gs, seat_inside_of->shape_parent_entity); - - cpVect target_direction = {0}; - if (cpvlength(movement_this_tick) > 0.0) + if (player->input.take_over_squad == SquadNone) { - target_direction = cpvnormalize(movement_this_tick); + player->squad = SquadNone; } - BOXES_ITER(gs, cur, g) + else { - if (cur->box_type == BoxThruster) + bool squad_taken = false; + PLAYERS_ITER(gs->players, other_player) { - - double wanted_thrust = -cpvdot(target_direction, box_facing_vector(cur)); - wanted_thrust = clamp01(wanted_thrust); - cur->wanted_thrust = wanted_thrust; - } - if (cur->box_type == BoxGyroscope) - { - cur->wanted_thrust = rotation_this_tick; + 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; } - } - } - -#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}; - cpVect 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 *maybe_box_to_destroy = cpSpacePointQueryNearest(gs->space, (world_build), 0.01, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &info); - if (maybe_box_to_destroy != NULL) - { - Entity *cur_box = cp_shape_entity(maybe_box_to_destroy); - if (!cur_box->indestructible && !cur_box->is_platonic) + // 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) { - Entity *cur_grid = cp_body_entity(cpShapeGetBody(maybe_box_to_destroy)); - p->damage -= DAMAGE_TO_PLAYER_PER_BLOCK * ((BATTERY_CAPACITY - cur_box->energy_used) / BATTERY_CAPACITY); - grid_remove_box(gs, cur_grid, cur_box); + possibly_to_invite->squad_invited_to = player->squad; } - } - else if (box_unlocked(player, player->input.build_type)) - { - // creating a box - p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; - cpVect created_box_position; - if (p->damage < 1.0) // player can't create a box that kills them by making it + Entity *p = get_entity(gs, player->entity); + // player respawning + if (p == NULL) { - if (target_grid == NULL) + p = new_entity(gs); + create_player_entity(gs, p); + player->entity = get_id(gs, p); + Entity *medbay = get_entity(gs, player->last_used_medbay); + entity_ensure_in_orbit(gs, p); + if (medbay != NULL) { - Entity *new_grid = new_entity(gs); - grid_create(gs, new_grid); - entity_set_pos(new_grid, world_build); - cpBodySetVelocity(new_grid->body, (player_vel(gs, p))); - target_grid = new_grid; - created_box_position = (cpVect){0}; + exit_seat(gs, medbay, p); + p->damage = 0.95; } - else - { - created_box_position = grid_world_to_local(target_grid, world_build); - } - Entity *new_box = new_entity(gs); - box_create(gs, new_box, target_grid, created_box_position); - new_box->owning_squad = player->squad; - 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; - if (new_box->box_type == BoxScanner) - new_box->blueprints_learned = player->box_unlocks; - if (new_box->box_type == BoxBattery) - new_box->energy_used = BATTERY_CAPACITY; } - } - } -#endif - if (p->damage >= 1.0) - { - entity_destroy(gs, p); - player->entity = (EntityID){0}; - } - - p->damage = clamp01(p->damage); - } - - // process entities - for (size_t i = 0; i < gs->cur_next_entity; i++) - { - Entity *e = &gs->entities[i]; - if (!e->exists) - continue; + assert(p->is_player); + p->owning_squad = player->squad; - // instant death - { - cpFloat dist_from_center = cpvlengthsq((entity_pos(e))); - if (e->body != NULL && dist_from_center > (INSTANT_DEATH_DISTANCE_FROM_CENTER * INSTANT_DEATH_DISTANCE_FROM_CENTER)) - { - bool platonic_found = false; - if (e->is_grid) + if (p->squad_invited_to != SquadNone) { - BOXES_ITER(gs, cur_box, e) + if (player->input.accept_cur_squad_invite) { - if (cur_box->is_platonic) - { - platonic_found = true; - break; - } + 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; } } - if (platonic_found) + +#ifdef INFINITE_RESOURCES + p->damage = 0.0; +#endif + // update gold win condition + if (cpvlength(cpvsub((cpBodyGetPosition(p->body)), gs->goldpos)) < GOLD_COLLECT_RADIUS) { - cpBody *body = e->body; - cpBodySetVelocity(body, cpvmult(cpBodyGetVelocity(body), -0.5)); - cpVect rel_to_center = cpvsub(cpBodyGetPosition(body), (cpVect){0}); - cpBodySetPosition(body, cpvmult(cpvnormalize(rel_to_center), INSTANT_DEATH_DISTANCE_FROM_CENTER)); + p->goldness += 0.1; + p->damage = 0.0; + gs->goldpos = (cpVect){.x = hash11((float)time(gs)) * 20.0, .y = hash11((float)time(gs) - 13.6) * 20.0}; } - else +#if 1 + cpVect world_hand_pos = get_world_hand_pos(gs, &player->input, p); + if (player->input.seat_action) { - entity_destroy(gs, e); - } - continue; - } - } + 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, (world_hand_pos), 0.1, 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); - // sun processing for this current entity - { - SUNS_ITER(gs) - { - cpVect pos_rel_sun = (cpvsub(entity_pos(e), (entity_pos(i.sun)))); - cpFloat sqdist = cpvlengthsq(pos_rel_sun); - if (!e->is_grid) // grids aren't damaged (this edge case sucks!) - { - sqdist = cpvlengthsq(cpvsub((entity_pos(e)), (entity_pos(i.sun)))); - if (sqdist < (i.sun->sun_radius * i.sun->sun_radius)) + if (potential_seat->box_type == BoxScanner) // learn everything from the scanner + { + player->box_unlocks |= potential_seat->blueprints_learned; + } + if (potential_seat->box_type == BoxMerge) // disconnect! + { + potential_seat->wants_disconnect = true; + grid_correct_for_holes(gs, box_grid(potential_seat)); + assert(potential_seat->exists); + assert(potential_seat->is_box); + assert(potential_seat->box_type == BoxMerge); + } + 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); + if (potential_seat->box_type == BoxMedbay) + 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 { - e->damage += 10.0 * dt; + 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 - if (e->body != NULL) + // process movement { - cpVect accel = sun_gravity_accel_for_entity(e, i.sun); - cpVect new_vel = entity_vel(gs, e); - new_vel = cpvadd(new_vel, cpvmult(accel, dt)); - cpBodySetVelocity(e->body, (new_vel)); - } - } - } + // no cheating by making movement bigger than length 1 + cpVect movement_this_tick = (cpVect){0}; + double rotation_this_tick = 0.0; + if (cpvlength(player->input.movement) > 0.0) + { + movement_this_tick = cpvmult(cpvnormalize(player->input.movement), clamp(cpvlength(player->input.movement), 0.0, 1.0)); + player->input.movement = (cpVect){0}; + } + if (fabs(player->input.rotation) > 0.0) + { + rotation_this_tick = player->input.rotation; + if (rotation_this_tick > 1.0) + rotation_this_tick = 1.0; + if (rotation_this_tick < -1.0) + rotation_this_tick = -1.0; + player->input.rotation = 0.0; + } + Entity *seat_inside_of = get_entity(gs, p->currently_inside_of_box); - if (e->is_explosion) - { - e->explosion_progress += dt; - e->explosion_pos = cpvadd(e->explosion_pos, cpvmult(e->explosion_vel, dt)); - do_explosion(gs, e, dt); - if (e->explosion_progress >= EXPLOSION_TIME) - { - entity_destroy(gs, e); - } - } + // strange rare bug I saw happen, related to explosives, but no idea how to + // reproduce. @Robust put a breakpoint here, reproduce, and fix it! + if (seat_inside_of != NULL && !seat_inside_of->is_box) + { + Log("Strange thing happened where player was in non box seat!\n"); + seat_inside_of = NULL; + p->currently_inside_of_box = (EntityID){0}; + } - if (e->is_missile) - { - if (is_burning(e)) - { - e->time_burned_for += dt; - cpBodyApplyForceAtWorldPoint(e->body, (cpvspin((cpVect){.x = MISSILE_BURN_FORCE, .y = 0.0}, entity_rotation(e))), (entity_pos(e))); - } - if (e->damage >= MISSILE_DAMAGE_THRESHOLD && e->time_burned_for >= MISSILE_ARM_TIME) - { - Entity *explosion = new_entity(gs); - explosion->is_explosion = true; - explosion->explosion_pos = entity_pos(e); - explosion->explosion_vel = (cpBodyGetVelocity(e->body)); - explosion->explosion_push_strength = MISSILE_EXPLOSION_PUSH; - explosion->explosion_radius = MISSILE_EXPLOSION_RADIUS; - entity_destroy(gs, e); - } - } + if (seat_inside_of == NULL) + { + cpShapeSetFilter(p->shape, PLAYER_SHAPE_FILTER); + cpBodyApplyForceAtWorldPoint(p->body, (cpvmult(movement_this_tick, PLAYER_JETPACK_FORCE)), cpBodyGetPosition(p->body)); + cpBodySetTorque(p->body, rotation_this_tick * PLAYER_JETPACK_TORQUE); + p->damage += cpvlength(movement_this_tick) * dt * PLAYER_JETPACK_SPICE_PER_SECOND; + p->damage += fabs(rotation_this_tick) * dt * PLAYER_JETPACK_ROTATION_ENERGY_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, (entity_pos(seat_inside_of))); + cpBodySetVelocity(p->body, (box_vel(seat_inside_of))); - if (e->is_box) - { - if (e->is_platonic) - { - e->damage = 0.0; - gs->platonic_positions[(int)e->box_type] = entity_pos(e); - } - 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)); - explosion->explosion_push_strength = BOMB_EXPLOSION_PUSH; - explosion->explosion_radius = BOMB_EXPLOSION_RADIUS; - if (!e->is_platonic) - grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); - } - if (e->box_type == BoxMerge) - { - Entity *from_merge = e; - assert(from_merge != NULL); + // share cloaking with box + p->time_was_last_cloaked = seat_inside_of->time_was_last_cloaked; + p->last_cloaked_by_squad = seat_inside_of->last_cloaked_by_squad; + + // set thruster thrust from movement + if (seat_inside_of->box_type == BoxCockpit) + { + Entity *g = get_entity(gs, seat_inside_of->shape_parent_entity); - grid_to_exclude = box_grid(from_merge); - Entity *other_merge = closest_box_to_point_in_radius(gs, entity_pos(from_merge), MERGE_MAX_DIST, merge_filter); + cpVect target_direction = {0}; + if (cpvlength(movement_this_tick) > 0.0) + { + target_direction = cpvnormalize(movement_this_tick); + } + BOXES_ITER(gs, cur, g) + { + if (cur->box_type == BoxThruster) + { - if (other_merge == NULL && from_merge->wants_disconnect) - from_merge->wants_disconnect = false; + double wanted_thrust = -cpvdot(target_direction, box_facing_vector(cur)); + wanted_thrust = clamp01(wanted_thrust); + cur->wanted_thrust = wanted_thrust; + } + if (cur->box_type == BoxGyroscope) + { + cur->wanted_thrust = rotation_this_tick; + } + } + } + } + } - if (!from_merge->wants_disconnect && other_merge != NULL && !other_merge->wants_disconnect) +#if 1 // building + if (player->input.dobuild) { - assert(box_grid(from_merge) != box_grid(other_merge)); - - Entity *from_grid = box_grid(from_merge); - Entity *other_grid = box_grid(other_merge); + 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... - // the merges are near eachother, but are they facing eachother... - bool from_facing_other = cpvdot(box_facing_vector(from_merge), cpvnormalize(cpvsub(entity_pos(other_merge), entity_pos(from_merge)))) > 0.8; - bool other_facing_from = cpvdot(box_facing_vector(other_merge), cpvnormalize(cpvsub(entity_pos(from_merge), entity_pos(other_merge)))) > 0.8; + cpPointQueryInfo info = {0}; + cpVect world_build = world_hand_pos; - // using this stuff to detect if when the other grid's boxes are snapped, they'll be snapped - // to be next to the from merge box - cpVect actual_new_pos = grid_snapped_box_pos(from_grid, entity_pos(other_merge)); - cpVect needed_new_pos = cpvadd(entity_pos(from_merge), cpvmult(box_facing_vector(from_merge), BOX_SIZE)); - if (from_facing_other && other_facing_from && cpvnear(needed_new_pos, actual_new_pos, 0.01)) + // @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 *maybe_box_to_destroy = cpSpacePointQueryNearest(gs->space, (world_build), 0.01, cpShapeFilterNew(CP_NO_GROUP, CP_ALL_CATEGORIES, BOXES), &info); + if (maybe_box_to_destroy != NULL) { - // do the merge - cpVect facing_vector_needed = cpvmult(box_facing_vector(from_merge), -1.0); - cpVect current_facing_vector = box_facing_vector(other_merge); - double angle_diff = cpvanglediff(current_facing_vector, facing_vector_needed); - if (angle_diff == FLT_MIN) - angle_diff = 0.0; - assert(!isnan(angle_diff)); - - cpBodySetAngle(other_grid->body, cpBodyGetAngle(other_grid->body) + angle_diff); - - cpVect moved_because_angle_change = cpvsub(needed_new_pos, entity_pos(other_merge)); - cpBodySetPosition(other_grid->body, (cpvadd(entity_pos(other_grid), moved_because_angle_change))); - - // cpVect snap_movement_vect = cpvsub(actual_new_pos, entity_pos(other_merge)); - cpVect snap_movement_vect = (cpVect){0}; - - Entity *cur = get_entity(gs, other_grid->boxes); - - other_grid->boxes = (EntityID){0}; - while (cur != NULL) + Entity *cur_box = cp_shape_entity(maybe_box_to_destroy); + if (!cur_box->indestructible && !cur_box->is_platonic) { - Entity *next = get_entity(gs, cur->next_box); - cpVect world = entity_pos(cur); - enum CompassRotation new_rotation = facing_vector_to_compass(from_grid, other_grid, box_facing_vector(cur)); - cur->compass_rotation = new_rotation; - cpVect new_cur_pos = grid_snapped_box_pos(from_grid, cpvadd(snap_movement_vect, world)); - box_create(gs, cur, from_grid, grid_world_to_local(from_grid, new_cur_pos)); // destroys next/prev fields on cur - assert(box_grid(cur) == box_grid(from_merge)); - cur = next; + Entity *cur_grid = cp_body_entity(cpShapeGetBody(maybe_box_to_destroy)); + p->damage -= DAMAGE_TO_PLAYER_PER_BLOCK * ((BATTERY_CAPACITY - cur_box->energy_used) / BATTERY_CAPACITY); + grid_remove_box(gs, cur_grid, cur_box); } - entity_destroy(gs, other_grid); } - } - } - if (e->damage >= 1.0) - { - grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); - } - } - if (e->is_grid) - { - Entity *grid = e; - // calculate how much energy solar panels provide - double energy_to_add = 0.0; - BOXES_ITER(gs, cur_box, grid) - { - if (cur_box->box_type == BoxSolarPanel) - { - cur_box->sun_amount = 0.0; - SUNS_ITER(gs) + else if (box_unlocked(player, player->input.build_type)) { - double new_sun = clamp01(fabs(cpvdot(box_facing_vector(cur_box), cpvnormalize(cpvsub(entity_pos(i.sun), entity_pos(cur_box)))))); - - // less sun the farther away you are! - new_sun *= lerp(1.0, 0.0, clamp01(cpvlength(cpvsub(entity_pos(cur_box), entity_pos(i.sun))) / sun_dist_no_gravity(i.sun))); - cur_box->sun_amount += new_sun; + // creating a box + p->damage += DAMAGE_TO_PLAYER_PER_BLOCK; + cpVect created_box_position; + if (p->damage < 1.0) // player can't create a box that kills them by making it + { + if (target_grid == NULL) + { + Entity *new_grid = new_entity(gs); + grid_create(gs, new_grid); + entity_set_pos(new_grid, world_build); + cpBodySetVelocity(new_grid->body, (player_vel(gs, p))); + target_grid = new_grid; + created_box_position = (cpVect){0}; + } + else + { + created_box_position = grid_world_to_local(target_grid, world_build); + } + Entity *new_box = new_entity(gs); + box_create(gs, new_box, target_grid, created_box_position); + new_box->owning_squad = player->squad; + 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; + if (new_box->box_type == BoxScanner) + new_box->blueprints_learned = player->box_unlocks; + if (new_box->box_type == BoxBattery) + new_box->energy_used = BATTERY_CAPACITY; + } } - energy_to_add += cur_box->sun_amount * SOLAR_ENERGY_PER_SECOND * dt; } - } - - // apply all of the energy to all connected batteries - BOXES_ITER(gs, cur, grid) - { - if (energy_to_add <= 0.0) - break; - if (cur->box_type == BoxBattery) +#endif + if (p->damage >= 1.0) { - double 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; + entity_destroy(gs, p); + player->entity = (EntityID){0}; } - assert(energy_to_add >= 0.0); - } - // any energy_to_add existing now can also be used to power thrusters/medbay - double non_battery_energy_left_over = energy_to_add; + p->damage = clamp01(p->damage); + } + } - // use the energy, stored in the batteries, in various boxes - BOXES_ITER(gs, cur_box, grid) + PROFILE_SCOPE("process entities") + { + for (size_t i = 0; i < gs->cur_next_entity; i++) { - if (cur_box->box_type == BoxThruster) - { + Entity *e = &gs->entities[i]; + if (!e->exists) + continue; - double energy_to_consume = cur_box->wanted_thrust * THRUSTER_ENERGY_USED_PER_SECOND * dt; - if (energy_to_consume > 0.0) + PROFILE_SCOPE("instant death ") + { + cpFloat dist_from_center = cpvlengthsq((entity_pos(e))); + if (e->body != NULL && dist_from_center > (INSTANT_DEATH_DISTANCE_FROM_CENTER * INSTANT_DEATH_DISTANCE_FROM_CENTER)) { - cur_box->thrust = 0.0; - double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, energy_to_consume); - cur_box->thrust = (1.0 - energy_unconsumed / energy_to_consume) * cur_box->wanted_thrust; - if (cur_box->thrust >= 0.0) - cpBodyApplyForceAtWorldPoint(grid->body, (thruster_force(cur_box)), (entity_pos(cur_box))); + bool platonic_found = false; + if (e->is_grid) + { + BOXES_ITER(gs, cur_box, e) + { + if (cur_box->is_platonic) + { + platonic_found = true; + break; + } + } + } + if (platonic_found) + { + cpBody *body = e->body; + cpBodySetVelocity(body, cpvmult(cpBodyGetVelocity(body), -0.5)); + cpVect rel_to_center = cpvsub(cpBodyGetPosition(body), (cpVect){0}); + cpBodySetPosition(body, cpvmult(cpvnormalize(rel_to_center), INSTANT_DEATH_DISTANCE_FROM_CENTER)); + } + else + { + entity_destroy(gs, e); + } + continue; } } - if (cur_box->box_type == BoxGyroscope) + + // sun processing for this current entity + PROFILE_SCOPE("this entity sun processing") { - double energy_to_consume = fabs(cur_box->wanted_thrust * GYROSCOPE_ENERGY_USED_PER_SECOND * dt); - if (energy_to_consume > 0.0) + SUNS_ITER(gs) { - cur_box->thrust = 0.0; - double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, energy_to_consume); - cur_box->thrust = (1.0 - energy_unconsumed / energy_to_consume) * cur_box->wanted_thrust; - if (fabs(cur_box->thrust) >= 0.0) - cpBodySetTorque(grid->body, cpBodyGetTorque(grid->body) + cur_box->thrust * GYROSCOPE_TORQUE); + cpVect pos_rel_sun = (cpvsub(entity_pos(e), (entity_pos(i.sun)))); + cpFloat sqdist = cpvlengthsq(pos_rel_sun); + if (!e->is_grid) // grids aren't damaged (this edge case sucks!) + { + sqdist = cpvlengthsq(cpvsub((entity_pos(e)), (entity_pos(i.sun)))); + if (sqdist < (i.sun->sun_radius * i.sun->sun_radius)) + { + e->damage += 10.0 * dt; + } + } + + if (e->body != NULL) + { + cpVect accel = sun_gravity_accel_for_entity(e, i.sun); + cpVect new_vel = entity_vel(gs, e); + new_vel = cpvadd(new_vel, cpvmult(accel, dt)); + cpBodySetVelocity(e->body, (new_vel)); + } } } - if (cur_box->box_type == BoxMedbay) + + if (e->is_explosion) { - Entity *potential_meatbag_to_heal = get_entity(gs, cur_box->player_who_is_inside_of_me); - if (potential_meatbag_to_heal != NULL) + PROFILE_SCOPE("Explosion") { - double wanted_energy_use = fmin(potential_meatbag_to_heal->damage, PLAYER_ENERGY_RECHARGE_PER_SECOND * dt); - if (wanted_energy_use > 0.0) + e->explosion_progress += dt; + e->explosion_pos = cpvadd(e->explosion_pos, cpvmult(e->explosion_vel, dt)); + do_explosion(gs, e, dt); + if (e->explosion_progress >= EXPLOSION_TIME) { - double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, wanted_energy_use); - potential_meatbag_to_heal->damage -= (1.0 - energy_unconsumed / wanted_energy_use) * wanted_energy_use; + entity_destroy(gs, e); } } } - if (cur_box->box_type == BoxCloaking) + + if (e->is_missile) { - double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, CLOAKING_ENERGY_USE * dt); - if (energy_unconsumed >= CLOAKING_ENERGY_USE * dt) + PROFILE_SCOPE("Missile") { - cur_box->cloaking_power = lerp(cur_box->cloaking_power, 0.0, dt * 3.0); - } - else - { - cur_box->cloaking_power = lerp(cur_box->cloaking_power, 1.0, dt * 3.0); - cpBody *tmp = cpBodyNew(0.0, 0.0); - cpBodySetPosition(tmp, (entity_pos(cur_box))); - cpBodySetAngle(tmp, entity_rotation(cur_box)); - // subtract a little from the panel size so that boxes just at the boundary of the panel - // aren't (sometimes cloaked)/(sometimes not) from floating point imprecision - cpShape *box_shape = cpBoxShapeNew(tmp, CLOAKING_PANEL_SIZE - 0.03, CLOAKING_PANEL_SIZE - 0.03, 0.0); - cpSpaceShapeQuery(gs->space, box_shape, cloaking_shield_callback_func, (void *)cur_box); - cpShapeFree(box_shape); - cpBodyFree(tmp); + if (is_burning(e)) + { + e->time_burned_for += dt; + cpBodyApplyForceAtWorldPoint(e->body, (cpvspin((cpVect){.x = MISSILE_BURN_FORCE, .y = 0.0}, entity_rotation(e))), (entity_pos(e))); + } + if (e->damage >= MISSILE_DAMAGE_THRESHOLD && e->time_burned_for >= MISSILE_ARM_TIME) + { + Entity *explosion = new_entity(gs); + explosion->is_explosion = true; + explosion->explosion_pos = entity_pos(e); + explosion->explosion_vel = (cpBodyGetVelocity(e->body)); + explosion->explosion_push_strength = MISSILE_EXPLOSION_PUSH; + explosion->explosion_radius = MISSILE_EXPLOSION_RADIUS; + entity_destroy(gs, e); + } } } - if (cur_box->box_type == BoxMissileLauncher) - { - LauncherTarget target = missile_launcher_target(gs, cur_box); - if (cur_box->missile_construction_charge < 1.0) + if (e->is_box) + { + PROFILE_SCOPE("Box processing") { - double want_use_energy = dt * MISSILE_CHARGE_RATE; - double energy_charged = want_use_energy - batteries_use_energy(gs, grid, &non_battery_energy_left_over, want_use_energy); - cur_box->missile_construction_charge += energy_charged; - } + if (e->is_platonic) + { + e->damage = 0.0; + gs->platonic_positions[(int)e->box_type] = entity_pos(e); + } + 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)); + explosion->explosion_push_strength = BOMB_EXPLOSION_PUSH; + explosion->explosion_radius = BOMB_EXPLOSION_RADIUS; + if (!e->is_platonic) + grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); + } + if (e->box_type == BoxMerge) + { + Entity *from_merge = e; + assert(from_merge != NULL); - if (target.target_found && cur_box->missile_construction_charge >= 1.0) - { - cur_box->missile_construction_charge = 0.0; - Entity *new_missile = new_entity(gs); - create_missile(gs, new_missile); - new_missile->owning_squad = cur_box->owning_squad; // missiles have teams and attack eachother! - double missile_spawn_dist = sqrt((BOX_SIZE / 2.0) * (BOX_SIZE / 2.0) * 2.0) + MISSILE_COLLIDER_SIZE.x / 2.0 + 0.1; - cpBodySetPosition(new_missile->body, (cpvadd(entity_pos(cur_box), cpvspin((cpVect){.x = missile_spawn_dist, 0.0}, target.facing_angle)))); - cpBodySetAngle(new_missile->body, target.facing_angle); - cpBodySetVelocity(new_missile->body, (box_vel(cur_box))); + grid_to_exclude = box_grid(from_merge); + Entity *other_merge = closest_box_to_point_in_radius(gs, entity_pos(from_merge), MERGE_MAX_DIST, merge_filter); + + if (other_merge == NULL && from_merge->wants_disconnect) + from_merge->wants_disconnect = false; + + if (!from_merge->wants_disconnect && other_merge != NULL && !other_merge->wants_disconnect) + { + assert(box_grid(from_merge) != box_grid(other_merge)); + + Entity *from_grid = box_grid(from_merge); + Entity *other_grid = box_grid(other_merge); + + // the merges are near eachother, but are they facing eachother... + bool from_facing_other = cpvdot(box_facing_vector(from_merge), cpvnormalize(cpvsub(entity_pos(other_merge), entity_pos(from_merge)))) > 0.8; + bool other_facing_from = cpvdot(box_facing_vector(other_merge), cpvnormalize(cpvsub(entity_pos(from_merge), entity_pos(other_merge)))) > 0.8; + + // using this stuff to detect if when the other grid's boxes are snapped, they'll be snapped + // to be next to the from merge box + cpVect actual_new_pos = grid_snapped_box_pos(from_grid, entity_pos(other_merge)); + cpVect needed_new_pos = cpvadd(entity_pos(from_merge), cpvmult(box_facing_vector(from_merge), BOX_SIZE)); + if (from_facing_other && other_facing_from && cpvnear(needed_new_pos, actual_new_pos, 0.01)) + { + // do the merge + cpVect facing_vector_needed = cpvmult(box_facing_vector(from_merge), -1.0); + cpVect current_facing_vector = box_facing_vector(other_merge); + double angle_diff = cpvanglediff(current_facing_vector, facing_vector_needed); + if (angle_diff == FLT_MIN) + angle_diff = 0.0; + assert(!isnan(angle_diff)); + + cpBodySetAngle(other_grid->body, cpBodyGetAngle(other_grid->body) + angle_diff); + + cpVect moved_because_angle_change = cpvsub(needed_new_pos, entity_pos(other_merge)); + cpBodySetPosition(other_grid->body, (cpvadd(entity_pos(other_grid), moved_because_angle_change))); + + // cpVect snap_movement_vect = cpvsub(actual_new_pos, entity_pos(other_merge)); + cpVect snap_movement_vect = (cpVect){0}; + + Entity *cur = get_entity(gs, other_grid->boxes); + + other_grid->boxes = (EntityID){0}; + while (cur != NULL) + { + Entity *next = get_entity(gs, cur->next_box); + cpVect world = entity_pos(cur); + enum CompassRotation new_rotation = facing_vector_to_compass(from_grid, other_grid, box_facing_vector(cur)); + cur->compass_rotation = new_rotation; + cpVect new_cur_pos = grid_snapped_box_pos(from_grid, cpvadd(snap_movement_vect, world)); + box_create(gs, cur, from_grid, grid_world_to_local(from_grid, new_cur_pos)); // destroys next/prev fields on cur + assert(box_grid(cur) == box_grid(from_merge)); + cur = next; + } + entity_destroy(gs, other_grid); + } + } + } + if (e->damage >= 1.0) + { + grid_remove_box(gs, get_entity(gs, e->shape_parent_entity), e); + } } } - if (cur_box->box_type == BoxScanner) + + if (e->is_grid) { - // set the nearest platonic solid! only on server as only the server sees everything - if (gs->server_side_computing) + PROFILE_SCOPE("Grid processing") { - double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, SCANNER_ENERGY_USE * dt); - if (energy_unconsumed >= SCANNER_ENERGY_USE * dt) + Entity *grid = e; + // calculate how much energy solar panels provide + double energy_to_add = 0.0; + BOXES_ITER(gs, cur_box, grid) { - cur_box->platonic_detection_strength = 0.0; - cur_box->platonic_nearest_direction = (cpVect){0}; + if (cur_box->box_type == BoxSolarPanel) + { + cur_box->sun_amount = 0.0; + SUNS_ITER(gs) + { + double new_sun = clamp01(fabs(cpvdot(box_facing_vector(cur_box), cpvnormalize(cpvsub(entity_pos(i.sun), entity_pos(cur_box)))))); + + // less sun the farther away you are! + new_sun *= lerp(1.0, 0.0, clamp01(cpvlength(cpvsub(entity_pos(cur_box), entity_pos(i.sun))) / sun_dist_no_gravity(i.sun))); + cur_box->sun_amount += new_sun; + } + energy_to_add += cur_box->sun_amount * SOLAR_ENERGY_PER_SECOND * dt; + } } - else + + // apply all of the energy to all connected batteries + BOXES_ITER(gs, cur, grid) { - cpVect from_pos = entity_pos(cur_box); - cpVect nearest = {0}; - double nearest_dist = INFINITY; - for (int i = 0; i < MAX_BOX_TYPES; i++) + if (energy_to_add <= 0.0) + break; + if (cur->box_type == BoxBattery) + { + double 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.0); + } + + // any energy_to_add existing now can also be used to power thrusters/medbay + double non_battery_energy_left_over = energy_to_add; + + // use the energy, stored in the batteries, in various boxes + BOXES_ITER(gs, cur_box, grid) + { + if (cur_box->box_type == BoxThruster) + { + + double energy_to_consume = cur_box->wanted_thrust * THRUSTER_ENERGY_USED_PER_SECOND * dt; + if (energy_to_consume > 0.0) + { + cur_box->thrust = 0.0; + double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, energy_to_consume); + cur_box->thrust = (1.0 - energy_unconsumed / energy_to_consume) * cur_box->wanted_thrust; + if (cur_box->thrust >= 0.0) + cpBodyApplyForceAtWorldPoint(grid->body, (thruster_force(cur_box)), (entity_pos(cur_box))); + } + } + if (cur_box->box_type == BoxGyroscope) + { + double energy_to_consume = fabs(cur_box->wanted_thrust * GYROSCOPE_ENERGY_USED_PER_SECOND * dt); + if (energy_to_consume > 0.0) + { + cur_box->thrust = 0.0; + double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, energy_to_consume); + cur_box->thrust = (1.0 - energy_unconsumed / energy_to_consume) * cur_box->wanted_thrust; + if (fabs(cur_box->thrust) >= 0.0) + cpBodySetTorque(grid->body, cpBodyGetTorque(grid->body) + cur_box->thrust * GYROSCOPE_TORQUE); + } + } + if (cur_box->box_type == BoxMedbay) { - cpVect cur_pos = gs->platonic_positions[i]; - if (cpvlength(cur_pos) > 0.0) // zero is uninitialized, the platonic solid doesn't exist (probably) @Robust do better + Entity *potential_meatbag_to_heal = get_entity(gs, cur_box->player_who_is_inside_of_me); + if (potential_meatbag_to_heal != NULL) { - double length_to_cur = cpvdist(from_pos, cur_pos); - if (length_to_cur < nearest_dist) + double wanted_energy_use = fmin(potential_meatbag_to_heal->damage, PLAYER_ENERGY_RECHARGE_PER_SECOND * dt); + if (wanted_energy_use > 0.0) { - nearest_dist = length_to_cur; - nearest = cur_pos; + double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, wanted_energy_use); + potential_meatbag_to_heal->damage -= (1.0 - energy_unconsumed / wanted_energy_use) * wanted_energy_use; } } } - if (nearest_dist < INFINITY) + if (cur_box->box_type == BoxCloaking) { - cur_box->platonic_nearest_direction = cpvnormalize(cpvsub(nearest, from_pos)); - cur_box->platonic_detection_strength = fmax(0.1, 1.0 - fmin(1.0, nearest_dist / 100.0)); + double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, CLOAKING_ENERGY_USE * dt); + if (energy_unconsumed >= CLOAKING_ENERGY_USE * dt) + { + cur_box->cloaking_power = lerp(cur_box->cloaking_power, 0.0, dt * 3.0); + } + else + { + cur_box->cloaking_power = lerp(cur_box->cloaking_power, 1.0, dt * 3.0); + cpBody *tmp = cpBodyNew(0.0, 0.0); + cpBodySetPosition(tmp, (entity_pos(cur_box))); + cpBodySetAngle(tmp, entity_rotation(cur_box)); + // subtract a little from the panel size so that boxes just at the boundary of the panel + // aren't (sometimes cloaked)/(sometimes not) from floating point imprecision + cpShape *box_shape = cpBoxShapeNew(tmp, CLOAKING_PANEL_SIZE - 0.03, CLOAKING_PANEL_SIZE - 0.03, 0.0); + cpSpaceShapeQuery(gs->space, box_shape, cloaking_shield_callback_func, (void *)cur_box); + cpShapeFree(box_shape); + cpBodyFree(tmp); + } } - else + if (cur_box->box_type == BoxMissileLauncher) { - cur_box->platonic_nearest_direction = (cpVect){0}; - cur_box->platonic_detection_strength = 0.0; + LauncherTarget target = missile_launcher_target(gs, cur_box); + + if (cur_box->missile_construction_charge < 1.0) + { + double want_use_energy = dt * MISSILE_CHARGE_RATE; + double energy_charged = want_use_energy - batteries_use_energy(gs, grid, &non_battery_energy_left_over, want_use_energy); + cur_box->missile_construction_charge += energy_charged; + } + + if (target.target_found && cur_box->missile_construction_charge >= 1.0) + { + cur_box->missile_construction_charge = 0.0; + Entity *new_missile = new_entity(gs); + create_missile(gs, new_missile); + new_missile->owning_squad = cur_box->owning_squad; // missiles have teams and attack eachother! + double missile_spawn_dist = sqrt((BOX_SIZE / 2.0) * (BOX_SIZE / 2.0) * 2.0) + MISSILE_COLLIDER_SIZE.x / 2.0 + 0.1; + cpBodySetPosition(new_missile->body, (cpvadd(entity_pos(cur_box), cpvspin((cpVect){.x = missile_spawn_dist, 0.0}, target.facing_angle)))); + cpBodySetAngle(new_missile->body, target.facing_angle); + cpBodySetVelocity(new_missile->body, (box_vel(cur_box))); + } } - } - } + if (cur_box->box_type == BoxScanner) + { + // set the nearest platonic solid! only on server as only the server sees everything + if (gs->server_side_computing) + { + double energy_unconsumed = batteries_use_energy(gs, grid, &non_battery_energy_left_over, SCANNER_ENERGY_USE * dt); + if (energy_unconsumed >= SCANNER_ENERGY_USE * dt) + { + cur_box->platonic_detection_strength = 0.0; + cur_box->platonic_nearest_direction = (cpVect){0}; + } + else + { + cpVect from_pos = entity_pos(cur_box); + cpVect nearest = {0}; + double nearest_dist = INFINITY; + for (int i = 0; i < MAX_BOX_TYPES; i++) + { + cpVect cur_pos = gs->platonic_positions[i]; + if (cpvlength(cur_pos) > 0.0) // zero is uninitialized, the platonic solid doesn't exist (probably) @Robust do better + { + double length_to_cur = cpvdist(from_pos, cur_pos); + if (length_to_cur < nearest_dist) + { + nearest_dist = length_to_cur; + nearest = cur_pos; + } + } + } + if (nearest_dist < INFINITY) + { + cur_box->platonic_nearest_direction = cpvnormalize(cpvsub(nearest, from_pos)); + cur_box->platonic_detection_strength = fmax(0.1, 1.0 - fmin(1.0, nearest_dist / 100.0)); + } + else + { + cur_box->platonic_nearest_direction = (cpVect){0}; + cur_box->platonic_detection_strength = 0.0; + } + } + } - // unlock the nearest platonic solid! - scanner_has_learned = cur_box->blueprints_learned; - Entity *to_learn = closest_box_to_point_in_radius(gs, entity_pos(cur_box), SCANNER_RADIUS, scanner_filter); - if (to_learn != NULL) - assert(to_learn->is_box); + // unlock the nearest platonic solid! + scanner_has_learned = cur_box->blueprints_learned; + Entity *to_learn = closest_box_to_point_in_radius(gs, entity_pos(cur_box), SCANNER_RADIUS, scanner_filter); + if (to_learn != NULL) + assert(to_learn->is_box); - EntityID new_id = get_id(gs, to_learn); + EntityID new_id = get_id(gs, to_learn); - if (!entityids_same(cur_box->currently_scanning, new_id)) - { - cur_box->currently_scanning_progress = 0.0; - cur_box->currently_scanning = new_id; - } + if (!entityids_same(cur_box->currently_scanning, new_id)) + { + cur_box->currently_scanning_progress = 0.0; + cur_box->currently_scanning = new_id; + } - double target_head_rotate_speed = cur_box->platonic_detection_strength > 0.0 ? 3.0 : 0.0; - if (to_learn != NULL) - { - cur_box->currently_scanning_progress += dt * SCANNER_SCAN_RATE; - target_head_rotate_speed *= 30.0 * cur_box->currently_scanning_progress; - } - else - cur_box->currently_scanning_progress = 0.0; + double target_head_rotate_speed = cur_box->platonic_detection_strength > 0.0 ? 3.0 : 0.0; + if (to_learn != NULL) + { + cur_box->currently_scanning_progress += dt * SCANNER_SCAN_RATE; + target_head_rotate_speed *= 30.0 * cur_box->currently_scanning_progress; + } + else + cur_box->currently_scanning_progress = 0.0; - if (cur_box->currently_scanning_progress >= 1.0) - { - cur_box->blueprints_learned |= box_unlock_number(to_learn->box_type); - } + if (cur_box->currently_scanning_progress >= 1.0) + { + cur_box->blueprints_learned |= box_unlock_number(to_learn->box_type); + } - cur_box->scanner_head_rotate_speed = lerp(cur_box->scanner_head_rotate_speed, target_head_rotate_speed, dt * 3.0); - cur_box->scanner_head_rotate += cur_box->scanner_head_rotate_speed * dt; - cur_box->scanner_head_rotate = fmod(cur_box->scanner_head_rotate, 2.0 * PI); + cur_box->scanner_head_rotate_speed = lerp(cur_box->scanner_head_rotate_speed, target_head_rotate_speed, dt * 3.0); + cur_box->scanner_head_rotate += cur_box->scanner_head_rotate_speed * dt; + cur_box->scanner_head_rotate = fmod(cur_box->scanner_head_rotate, 2.0 * PI); + } + } + } } } } - } - cpSpaceStep(gs->space, dt); + PROFILE_SCOPE("chipmunk physics processing") + { + cpSpaceStep(gs->space, dt); + } + } } diff --git a/main.c b/main.c index 6a8a40f..99080bd 100644 --- a/main.c +++ b/main.c @@ -32,6 +32,9 @@ #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" +#include "ipsettings.h" +#include "profiling.h" + // shaders #include "goodpixel.gen.h" #include "hueshift.gen.h" @@ -663,6 +666,7 @@ static void init(void) ENetEvent event; enet_address_set_host(&address, SERVER_ADDRESS); + Log("Connecting to %s:%d\n", SERVER_ADDRESS, SERVER_PORT); address.port = SERVER_PORT; peer = enet_host_connect(client, &address, 2, 0); if (peer == NULL) @@ -1386,739 +1390,760 @@ static cpVect get_global_hand_pos(cpVect world_mouse_pos, bool *hand_at_arms_len } static void frame(void) { - double width = (float)sapp_width(), height = (float)sapp_height(); - double ratio = width / height; - double time = sapp_frame_count() * sapp_frame_duration(); - double dt = (float)sapp_frame_duration(); - - // pressed input management + PROFILE_SCOPE("frame") { - for (int i = 0; i < MAX_KEYDOWN; i++) + double width = (float)sapp_width(), height = (float)sapp_height(); + double ratio = width / height; + double exec_time = sapp_frame_count() * sapp_frame_duration(); + double dt = (float)sapp_frame_duration(); + + // pressed input management { - if (keypressed[i].frame < sapp_frame_count()) + for (int i = 0; i < MAX_KEYDOWN; i++) { - keypressed[i].pressed = false; + 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()) + for (int i = 0; i < MAX_MOUSEBUTTON; i++) { - mousepressed[i].pressed = false; + 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; + build_pressed = mousepressed[SAPP_MOUSEBUTTON_LEFT].pressed; + interact_pressed = mousepressed[SAPP_MOUSEBUTTON_RIGHT].pressed; - // networking - { - ENetEvent event; - while (true) + // networking + static cpVect before_reprediction = {0}; + PROFILE_SCOPE("networking") { - int enet_status = enet_host_service(client, &event, 0); - if (enet_status > 0) + ENetEvent event; + while (true) { - switch (event.type) + int enet_status = enet_host_service(client, &event, 0); + if (enet_status > 0) { - case ENET_EVENT_TYPE_NONE: - { - Log("Wtf none event type?\n"); - break; - } - case ENET_EVENT_TYPE_CONNECT: - { - Log("New client from host %x\n", event.peer->address.host); - break; - } - - case ENET_EVENT_TYPE_RECEIVE: - { - unsigned 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) + switch (event.type) { - server_to_client_deserialize(&msg, decompressed, - decompressed_max_len, false); - my_player_index = msg.your_player; - } - else + case ENET_EVENT_TYPE_NONE: { - 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.1; - } - else if (predicted_to_time - gs.time > - target_prediction_time + eps * 2.0) - { - target_prediction_time_factor = 0.9; + Log("Wtf none event type?\n"); + break; } - else + case ENET_EVENT_TYPE_CONNECT: { - target_prediction_time_factor = 1.0; + Log("New client from host %x\n", event.peer->address.host); + break; } - // re-predict the inputs - double time_to_repredict = (float)difference; - uint64_t start_prediction_time = stm_now(); - if (time_to_repredict > 0.0) + case ENET_EVENT_TYPE_RECEIVE: { - while (time_to_repredict > TIMESTEP) + unsigned 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 = time(&gs); + 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) { - if (stm_ms(stm_diff(stm_now(), start_prediction_time)) > MAX_MS_SPENT_REPREDICTING) + if (myentity() != NULL) + before_reprediction = entity_pos(myentity()); + PROFILE_SCOPE("Deserializing data") + { + 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); + + PROFILE_SCOPE("Repredicting inputs") + { + + double server_current_time = time(&gs); + 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 - time(&gs) < target_prediction_time - eps) { - Log("Reprediction took longer than %f milliseconds", MAX_MS_SPENT_REPREDICTING); + target_prediction_time_factor = 1.1; } - QUEUE_ITER(&input_queue, cur_header) + else if (predicted_to_time - time(&gs) > + target_prediction_time + eps * 2.0) { - InputFrame *cur = (InputFrame *)cur_header->data; - if (cur->tick == tick(&gs)) + target_prediction_time_factor = 0.9; + } + else + { + target_prediction_time_factor = 1.0; + } + + // re-predict the inputs + double time_to_repredict = (float)difference; + Log("Repredicting %f\n", time_to_repredict); + + uint64_t start_prediction_time = stm_now(); + if (time_to_repredict > 0.0) + { + while (time_to_repredict > TIMESTEP) { - myplayer()->input = *cur; - break; + if (stm_ms(stm_diff(stm_now(), start_prediction_time)) > MAX_MS_SPENT_REPREDICTING) + { + Log("Reprediction took longer than %f milliseconds, could only predict %f\n", MAX_MS_SPENT_REPREDICTING, time_to_repredict); + break; + } + QUEUE_ITER(&input_queue, cur_header) + { + InputFrame *cur = (InputFrame *)cur_header->data; + if (cur->tick == tick(&gs)) + { + myplayer()->input = *cur; + break; + } + } + process(&gs, TIMESTEP, false); + time_to_repredict -= TIMESTEP; } + process(&gs, time_to_repredict, true); + time_to_repredict = 0.0; } - process(&gs, TIMESTEP); - time_to_repredict -= TIMESTEP; + + current_time_ahead_of_server = time(&gs) - server_current_time; } - process(&gs, time_to_repredict); - time_to_repredict = 0.0; + break; } - 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; + } + } } - - case ENET_EVENT_TYPE_DISCONNECT: + else if (enet_status == 0) { - fprintf(stderr, "Disconnected from server\n"); - exit(-1); break; } + else if (enet_status < 0) + { + fprintf(stderr, "Error receiving enet events: %d\n", enet_status); + 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 - cpVect build_target_pos = {0}; - double build_target_rotation = 0.0; - struct BuildPreviewInfo - { - cpVect grid_pos; - double grid_rotation; - } build_preview = {0}; - cpVect global_hand_pos = {0}; // world coords! world star! - bool hand_at_arms_length = false; - recalculate_camera_pos(); - cpVect world_mouse_pos = screen_to_world(width, height, mouse_pos); - { - // interpolate zoom - zoom = lerp(zoom, zoom_target, dt * 12.0); - - // calculate build preview stuff - cpVect local_hand_pos = {0}; - global_hand_pos = - get_global_hand_pos(world_mouse_pos, &hand_at_arms_length); + // dbg_rect(before_reprediction); - if (myentity() != NULL) + // 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 + cpVect build_target_pos = {0}; + double build_target_rotation = 0.0; + struct BuildPreviewInfo { - local_hand_pos = cpvsub(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), cpvmult(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; + cpVect grid_pos; + double grid_rotation; + } build_preview = {0}; + cpVect global_hand_pos = {0}; // world coords! world star! + bool hand_at_arms_length = false; + recalculate_camera_pos(); + cpVect world_mouse_pos = screen_to_world(width, height, mouse_pos); + PROFILE_SCOPE("gameplay and prediction") { - // 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. - - cpVect input = (cpVect){ - .x = (float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A], - .y = (float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S], - }; - if (cpvlength(input) > 0.0) - input = cpvnormalize(input); - cur_input_frame.movement = input; - cur_input_frame.rotation = (float)keydown[SAPP_KEYCODE_E] - (float)keydown[SAPP_KEYCODE_Q]; + // interpolate zoom + zoom = lerp(zoom, zoom_target, dt * 12.0); - if (interact_pressed) - cur_input_frame.seat_action = interact_pressed; + // calculate build preview stuff + cpVect local_hand_pos = {0}; + global_hand_pos = + get_global_hand_pos(world_mouse_pos, &hand_at_arms_length); - 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) + if (myentity() != NULL) { - cur_input_frame.reject_cur_squad_invite = true; - reject_invite = false; + local_hand_pos = cpvsub(global_hand_pos, entity_pos(myentity())); } - if (build_pressed && currently_building() != BoxInvalid) - { - cur_input_frame.dobuild = build_pressed; - cur_input_frame.build_type = currently_building(); - cur_input_frame.build_rotation = cur_editing_rotation; - } + // 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), cpvmult(PLAYER_SIZE, player_scaling)), world_mouse_pos)) + { + maybe_inviting_this_player = get_id(&gs, cur); + interact_pressed = false; + } + } - // "commit" the input. each input must be on a successive tick. - if (tick(&gs) > last_input_committed_tick) + // 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; { - cur_input_frame.tick = tick(&gs); - last_input_committed_tick = tick(&gs); + // 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. + + cpVect input = (cpVect){ + .x = (float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A], + .y = (float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S], + }; + if (cpvlength(input) > 0.0) + input = cpvnormalize(input); + cur_input_frame.movement = input; + cur_input_frame.rotation = (float)keydown[SAPP_KEYCODE_E] - (float)keydown[SAPP_KEYCODE_Q]; - InputFrame *to_push_to = queue_push_element(&input_queue); - if (to_push_to == NULL) + 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) { - InputFrame *to_discard = queue_pop_element(&input_queue); - (void)to_discard; - to_push_to = queue_push_element(&input_queue); - assert(to_push_to != NULL); + 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; } - *to_push_to = cur_input_frame; + if (build_pressed && currently_building() != BoxInvalid) + { + cur_input_frame.dobuild = build_pressed; + cur_input_frame.build_type = currently_building(); + cur_input_frame.build_rotation = cur_editing_rotation; + } - if (myplayer() != NULL) - myplayer()->input = - cur_input_frame; // for the client side prediction! + // "commit" the input. each input must be on a successive tick. + if (tick(&gs) > last_input_committed_tick) + { + cur_input_frame.tick = tick(&gs); + last_input_committed_tick = tick(&gs); - cur_input_frame = (InputFrame){0}; - cur_input_frame.take_over_squad = - -1; // @Robust make this zero initialized - } + 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); + } - // in client side prediction, only process the latest in the queue, not - // the one currently constructing. - static double prediction_time_factor = 1.0; - prediction_time_factor = lerp(prediction_time_factor, - target_prediction_time_factor, dt * 3.0); - process(&gs, dt * prediction_time_factor); + *to_push_to = cur_input_frame; - 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, - }; - unsigned 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)) - { - unsigned 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(); + 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 } - else + + // in client side prediction, only process the latest in the queue, not + // the one currently constructing. + static double prediction_time_factor = 1.0; + prediction_time_factor = lerp(prediction_time_factor, + target_prediction_time_factor, dt * 3.0); + process(&gs, dt * prediction_time_factor, true); + + static int64_t last_sent_input_time = 0; + if (stm_sec(stm_diff(stm_now(), last_sent_input_time)) > + TIME_BETWEEN_INPUT_PACKETS) { - Log("Failed to serialize client to server!\n"); + ma_mutex_lock(&send_packets_mutex); + ClientToServer to_send = { + .mic_data = &packets_to_send, + .input_data = &input_queue, + }; + unsigned 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)) + { + unsigned 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); } - 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); + // 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 = box_grid(closest_box_to_point_in_radius( - &gs, global_hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP, NULL)); - if (placing_grid == NULL) - { - build_preview = (struct BuildPreviewInfo){ - .grid_pos = global_hand_pos, - .grid_rotation = 0.0, - }; - } - 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, (float)width, 0.0f, (float)height); - sgp_set_blend_mode(SGP_BLENDMODE_BLEND); - - // Draw background color - set_color(colhexcode(0x000000)); - // set_color_values(0.1, 0.1, 0.1, 1.0); - sgp_clear(); - - // WORLD SPACE - // world space coordinates are +Y up, -Y down. Like normal cartesian coords - transform_scope - { - translate(width / 2, height / 2); - scale_at(zoom, -zoom, 0.0, 0.0); - - // parllax layers, just the zooming, but not 100% of the camera panning -#if 1 // space background - transform_scope + Entity *placing_grid = box_grid(closest_box_to_point_in_radius( + &gs, global_hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP, NULL)); + if (placing_grid == NULL) { - cpVect scaled_camera_pos = cpvmult( - camera_pos, 0.0005); // this is how strong/weak the parallax is - translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - sgp_set_image(0, image_stars); - double stars_height_over_width = - (float)sg_query_image_info(image_stars).height / - (float)sg_query_image_info(image_stars).width; - const double stars_width = 35.0; - double stars_height = stars_width * stars_height_over_width; - pipeline_scope(goodpixel_pipeline) - draw_textured_rect(-stars_width / 2.0, -stars_height / 2.0, stars_width, stars_height); - // draw_textured_rect(0, 0, stars_width, stars_height); - sgp_reset_image(0); + build_preview = (struct BuildPreviewInfo){ + .grid_pos = global_hand_pos, + .grid_rotation = 0.0, + }; } - transform_scope + else { - cpVect scaled_camera_pos = cpvmult( - camera_pos, 0.005); // this is how strong/weak the parallax is - translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - sgp_set_image(0, image_stars2); - double stars_height_over_width = - (float)sg_query_image_info(image_stars).height / - (float)sg_query_image_info(image_stars).width; - const double stars_width = 35.0; - double stars_height = stars_width * stars_height_over_width; - pipeline_scope(goodpixel_pipeline) - draw_textured_rect(-stars_width / 2.0, -stars_height / 2.0, stars_width, stars_height); - // draw_textured_rect(0, 0, stars_width, stars_height); - sgp_reset_image(0); + 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), + }; } -#endif + } -#if 1 // parallaxed dots - transform_scope - { - cpVect scaled_camera_pos = cpvmult(camera_pos, 0.25); - translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - draw_dots(scaled_camera_pos, 3.0); - } + // drawing + PROFILE_SCOPE("drawing") + { + sgp_begin((int)width, (int)height); + sgp_viewport(0, 0, (int)width, (int)height); + sgp_project(0.0f, (float)width, 0.0f, (float)height); + sgp_set_blend_mode(SGP_BLENDMODE_BLEND); + + // Draw background color + set_color(colhexcode(0x000000)); + // set_color_values(0.1, 0.1, 0.1, 1.0); + sgp_clear(); + + // WORLD SPACE + // world space coordinates are +Y up, -Y down. Like normal cartesian coords transform_scope { - cpVect scaled_camera_pos = cpvmult(camera_pos, 0.5); - translate(-scaled_camera_pos.x, -scaled_camera_pos.y); - set_color(WHITE); - draw_dots(scaled_camera_pos, 2.0); - } + translate(width / 2, height / 2); + scale_at(zoom, -zoom, 0.0, 0.0); + + // parllax layers, just the zooming, but not 100% of the camera panning +#if 1 // space background + transform_scope + { + cpVect scaled_camera_pos = cpvmult( + camera_pos, 0.0005); // this is how strong/weak the parallax is + translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + sgp_set_image(0, image_stars); + double stars_height_over_width = + (float)sg_query_image_info(image_stars).height / + (float)sg_query_image_info(image_stars).width; + const double stars_width = 35.0; + double stars_height = stars_width * stars_height_over_width; + pipeline_scope(goodpixel_pipeline) + draw_textured_rect(-stars_width / 2.0, -stars_height / 2.0, stars_width, stars_height); + // draw_textured_rect(0, 0, stars_width, stars_height); + sgp_reset_image(0); + } + transform_scope + { + cpVect scaled_camera_pos = cpvmult( + camera_pos, 0.005); // this is how strong/weak the parallax is + translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + sgp_set_image(0, image_stars2); + double stars_height_over_width = + (float)sg_query_image_info(image_stars).height / + (float)sg_query_image_info(image_stars).width; + const double stars_width = 35.0; + double stars_height = stars_width * stars_height_over_width; + pipeline_scope(goodpixel_pipeline) + draw_textured_rect(-stars_width / 2.0, -stars_height / 2.0, stars_width, stars_height); + // draw_textured_rect(0, 0, stars_width, stars_height); + sgp_reset_image(0); + } #endif - // camera go to player - translate(-camera_pos.x, -camera_pos.y); +#if 1 // parallaxed dots + transform_scope + { + cpVect scaled_camera_pos = cpvmult(camera_pos, 0.25); + translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + draw_dots(scaled_camera_pos, 3.0); + } + transform_scope + { + cpVect scaled_camera_pos = cpvmult(camera_pos, 0.5); + translate(-scaled_camera_pos.x, -scaled_camera_pos.y); + set_color(WHITE); + draw_dots(scaled_camera_pos, 2.0); + } +#endif - draw_dots(camera_pos, 1.5); // in plane dots + // camera go to player + translate(-camera_pos.x, -camera_pos.y); - // hand reached limit circle - if (myentity() != NULL) - { - static double hand_reach_alpha = 1.0; - hand_reach_alpha = lerp(hand_reach_alpha, - hand_at_arms_length ? 1.0 : 0.0, dt * 5.0); - set_color_values(1.0, 1.0, 1.0, hand_reach_alpha); - draw_circle(entity_pos(myentity()), MAX_HAND_REACH); - } + draw_dots(camera_pos, 1.5); // in plane dots - // vision circle, what player can see - if (myentity() != NULL) - { - set_color(colhexcode(0x4685e3)); - draw_circle(entity_pos(myentity()), VISION_RADIUS); - } + // hand reached limit circle + if (myentity() != NULL) + { + static double hand_reach_alpha = 1.0; + hand_reach_alpha = lerp(hand_reach_alpha, + hand_at_arms_length ? 1.0 : 0.0, dt * 5.0); + set_color_values(1.0, 1.0, 1.0, hand_reach_alpha); + draw_circle(entity_pos(myentity()), MAX_HAND_REACH); + } - double halfbox = BOX_SIZE / 2.0; + // vision circle, what player can see + if (myentity() != NULL) + { + set_color(colhexcode(0x4685e3)); + draw_circle(entity_pos(myentity()), VISION_RADIUS); + } - // mouse frozen, debugging tool - if (mouse_frozen) - { - set_color_values(1.0, 0.0, 0.0, 0.5); - draw_filled_rect(world_mouse_pos.x, world_mouse_pos.y, 0.1, 0.1); - } + double halfbox = BOX_SIZE / 2.0; - // building preview - if (currently_building() != BoxInvalid && can_build(currently_building())) - { - set_color_values(0.5, 0.5, 0.5, - (sin((float)time * 9.0) + 1.0) / 3.0 + 0.2); + // mouse frozen, debugging tool + if (mouse_frozen) + { + set_color_values(1.0, 0.0, 0.0, 0.5); + draw_filled_rect(world_mouse_pos.x, world_mouse_pos.y, 0.1, 0.1); + } - transform_scope + // building preview + if (currently_building() != BoxInvalid && can_build(currently_building())) { - sgp_set_image(0, boxinfo(currently_building()).image); - rotate_at(build_preview.grid_rotation + - rotangle(cur_editing_rotation), - global_hand_pos.x, global_hand_pos.y); - pipeline_scope(goodpixel_pipeline) - draw_texture_centered(global_hand_pos, BOX_SIZE); - // drawbox(hand_pos, build_preview.grid_rotation, 0.0, - // cur_editing_boxtype, cur_editing_rotation); - sgp_reset_image(0); + set_color_values(0.5, 0.5, 0.5, + (sin((float)exec_time * 9.0) + 1.0) / 3.0 + 0.2); + + transform_scope + { + sgp_set_image(0, boxinfo(currently_building()).image); + rotate_at(build_preview.grid_rotation + + rotangle(cur_editing_rotation), + global_hand_pos.x, global_hand_pos.y); + pipeline_scope(goodpixel_pipeline) + draw_texture_centered(global_hand_pos, BOX_SIZE); + // drawbox(hand_pos, build_preview.grid_rotation, 0.0, + // cur_editing_boxtype, cur_editing_rotation); + sgp_reset_image(0); + } } - } - player_scaling = - lerp(player_scaling, zoom < 6.5 ? 100.0 : 1.0, dt * 7.0); - ENTITIES_ITER(e) - { - // draw grid - if (e->is_grid) + player_scaling = + lerp(player_scaling, zoom < 6.5 ? 100.0 : 1.0, dt * 7.0); + ENTITIES_ITER(e) { - Entity *g = e; - BOXES_ITER(&gs, b, g) + // draw grid + if (e->is_grid) { - set_color_values(1.0, 1.0, 1.0, 1.0); - if (b->box_type == BoxBattery) - { - double 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); - set_color_values(result.r, result.g, result.b, cur_alpha); - } - transform_scope + Entity *g = e; + BOXES_ITER(&gs, b, g) { - rotate_at(entity_rotation(g) + rotangle(b->compass_rotation), - entity_pos(b).x, entity_pos(b).y); - - if (b->box_type == BoxThruster) + set_color_values(1.0, 1.0, 1.0, 1.0); + if (b->box_type == BoxBattery) { - transform_scope + double 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); + set_color_values(result.r, result.g, result.b, cur_alpha); + } + transform_scope + { + rotate_at(entity_rotation(g) + rotangle(b->compass_rotation), + entity_pos(b).x, entity_pos(b).y); + + if (b->box_type == BoxThruster) { - set_color_values(1.0, 1.0, 1.0, 1.0); - sgp_set_image(0, image_thrusterburn); - double scaling = 0.95 + lerp(0.0, 0.3, b->thrust); - scale_at(scaling, 1.0, entity_pos(b).x, entity_pos(b).y); - pipeline_scope(goodpixel_pipeline) + transform_scope { - draw_texture_centered(entity_pos(b), BOX_SIZE); + set_color_values(1.0, 1.0, 1.0, 1.0); + sgp_set_image(0, image_thrusterburn); + double scaling = 0.95 + lerp(0.0, 0.3, b->thrust); + scale_at(scaling, 1.0, entity_pos(b).x, entity_pos(b).y); + pipeline_scope(goodpixel_pipeline) + { + 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; + } + if (b->box_type == BoxSolarPanel) + { + sgp_set_image(0, image_solarpanel_charging); + set_color_values(1.0, 1.0, 1.0, b->sun_amount); + pipeline_scope(goodpixel_pipeline) + draw_texture_centered(entity_pos(b), BOX_SIZE); sgp_reset_image(0); + set_color_values(1.0, 1.0, 1.0, 1.0 - b->sun_amount); + /* Color to_set = colhexcode(0xeb9834); + to_set.a = b->sun_amount * 0.5; + set_color(to_set); + draw_color_rect_centered(entity_pos(b), BOX_SIZE); + */ } - } - 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; - } - if (b->box_type == BoxSolarPanel) - { - sgp_set_image(0, image_solarpanel_charging); - set_color_values(1.0, 1.0, 1.0, b->sun_amount); - pipeline_scope(goodpixel_pipeline) + + sgp_set_image(0, img); + if (b->indestructible) + { + set_color_values(0.2, 0.2, 0.2, 1.0); + } + else if (b->is_platonic) + { + set_color(GOLD); + } + + // all of these box types show team colors so are drawn with the hue shifting shader + // used with the player + if (b->box_type == BoxCloaking || b->box_type == BoxMissileLauncher) + { + pipeline_scope(hueshift_pipeline) + { + setup_hueshift(b->owning_squad); draw_texture_centered(entity_pos(b), BOX_SIZE); + } + } + else + { + pipeline_scope(goodpixel_pipeline) + draw_texture_centered(entity_pos(b), BOX_SIZE); + } sgp_reset_image(0); - set_color_values(1.0, 1.0, 1.0, 1.0 - b->sun_amount); - /* Color to_set = colhexcode(0xeb9834); - to_set.a = b->sun_amount * 0.5; - set_color(to_set); + + if (b->box_type == BoxScanner) + { + sgp_set_image(0, image_scanner_head); + transform_scope + { + pipeline_scope(goodpixel_pipeline) + { + rotate_at(b->scanner_head_rotate, entity_pos(b).x, entity_pos(b).y); + draw_texture_centered(entity_pos(b), BOX_SIZE); + } + } + sgp_reset_image(0); + set_color(WHITE); + } + + // scanner range, visualizes what scanner can scan + if (b->box_type == BoxScanner) + { + set_color(BLUE); + draw_circle(entity_pos(b), SCANNER_RADIUS); + set_color(WHITE); + } + set_color_values(0.5, 0.1, 0.1, b->damage); draw_color_rect_centered(entity_pos(b), BOX_SIZE); - */ - } - sgp_set_image(0, img); - if (b->indestructible) - { - set_color_values(0.2, 0.2, 0.2, 1.0); - } - else if (b->is_platonic) - { - set_color(GOLD); + if (b->box_type == BoxCloaking) + { + set_color_values(1.0, 1.0, 1.0, b->cloaking_power); + sgp_set_image(0, image_cloaking_panel); + pipeline_scope(goodpixel_pipeline) + draw_texture_centered(entity_pos(b), CLOAKING_PANEL_SIZE); + sgp_reset_image(0); + } } - // all of these box types show team colors so are drawn with the hue shifting shader - // used with the player - if (b->box_type == BoxCloaking || b->box_type == BoxMissileLauncher) + // outside of the transform scope + if (b->box_type == BoxScanner) { - pipeline_scope(hueshift_pipeline) + if (b->platonic_detection_strength > 0.0) { - setup_hueshift(b->owning_squad); - draw_texture_centered(entity_pos(b), BOX_SIZE); + set_color(colhexcode(0xf2d75c)); + cpVect to = cpvadd(entity_pos(b), cpvmult(b->platonic_nearest_direction, b->platonic_detection_strength)); + dbg_rect(to); + dbg_rect(entity_pos(b)); + draw_line(entity_pos(b).x, entity_pos(b).y, to.x, to.y); } } - else + if (b->box_type == BoxMissileLauncher) { - pipeline_scope(goodpixel_pipeline) - draw_texture_centered(entity_pos(b), BOX_SIZE); - } - sgp_reset_image(0); + set_color(RED); + draw_circle(entity_pos(b), MISSILE_RANGE); - if (b->box_type == BoxScanner) - { - sgp_set_image(0, image_scanner_head); + // draw the charging missile transform_scope { - pipeline_scope(goodpixel_pipeline) + rotate_at(missile_launcher_target(&gs, b).facing_angle, entity_pos(b).x, entity_pos(b).y); + sgp_set_image(0, image_missile); + pipeline_scope(hueshift_pipeline) { - rotate_at(b->scanner_head_rotate, entity_pos(b).x, entity_pos(b).y); + set_color_values(1.0, 1.0, 1.0, b->missile_construction_charge); + setup_hueshift(b->owning_squad); draw_texture_centered(entity_pos(b), BOX_SIZE); } + sgp_reset_image(0); } - sgp_reset_image(0); - set_color(WHITE); } + } + } - // scanner range, visualizes what scanner can scan - if (b->box_type == BoxScanner) + // draw missile + if (e->is_missile) + { + transform_scope + { + rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); + set_color_values(1.0, 1.0, 1.0, 1.0); + + if (is_burning(e)) { - set_color(BLUE); - draw_circle(entity_pos(b), SCANNER_RADIUS); - set_color(WHITE); + sgp_set_image(0, image_missile_burning); } - set_color_values(0.5, 0.1, 0.1, b->damage); - draw_color_rect_centered(entity_pos(b), BOX_SIZE); - - if (b->box_type == BoxCloaking) + else { - set_color_values(1.0, 1.0, 1.0, b->cloaking_power); - sgp_set_image(0, image_cloaking_panel); - pipeline_scope(goodpixel_pipeline) - draw_texture_centered(entity_pos(b), CLOAKING_PANEL_SIZE); - sgp_reset_image(0); + sgp_set_image(0, image_missile); } - } - // outside of the transform scope - if (b->box_type == BoxScanner) - { - if (b->platonic_detection_strength > 0.0) + pipeline_scope(hueshift_pipeline) { - set_color(colhexcode(0xf2d75c)); - cpVect to = cpvadd(entity_pos(b), cpvmult(b->platonic_nearest_direction, b->platonic_detection_strength)); - dbg_rect(to); - dbg_rect(entity_pos(b)); - draw_line(entity_pos(b).x, entity_pos(b).y, to.x, to.y); + setup_hueshift(e->owning_squad); + draw_texture_rectangle_centered(entity_pos(e), MISSILE_SPRITE_SIZE); } + + sgp_reset_image(0); } - if (b->box_type == BoxMissileLauncher) + } + + // draw player + if (e->is_player && + get_entity(&gs, e->currently_inside_of_box) == NULL) + { + transform_scope { - set_color(RED); - draw_circle(entity_pos(b), MISSILE_RANGE); + rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); + set_color_values(1.0, 1.0, 1.0, 1.0); - // draw the charging missile - transform_scope + pipeline_scope(hueshift_pipeline) { - rotate_at(missile_launcher_target(&gs, b).facing_angle, entity_pos(b).x, entity_pos(b).y); - sgp_set_image(0, image_missile); - pipeline_scope(hueshift_pipeline) - { - set_color_values(1.0, 1.0, 1.0, b->missile_construction_charge); - setup_hueshift(b->owning_squad); - draw_texture_centered(entity_pos(b), BOX_SIZE); - } + setup_hueshift(e->owning_squad); + sgp_set_image(0, image_player); + draw_texture_rectangle_centered( + entity_pos(e), cpvmult(PLAYER_SIZE, player_scaling)); sgp_reset_image(0); } } } - } - - // draw missile - if (e->is_missile) - { - transform_scope + if (e->is_explosion) { - rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); - set_color_values(1.0, 1.0, 1.0, 1.0); - - if (is_burning(e)) - { - sgp_set_image(0, image_missile_burning); - } - else - { - sgp_set_image(0, image_missile); - } - - pipeline_scope(hueshift_pipeline) - { - setup_hueshift(e->owning_squad); - draw_texture_rectangle_centered(entity_pos(e), MISSILE_SPRITE_SIZE); - } - + sgp_set_image(0, image_explosion); + set_color_values(1.0, 1.0, 1.0, + 1.0 - (e->explosion_progress / EXPLOSION_TIME)); + draw_texture_centered(e->explosion_pos, e->explosion_radius * 2.0); sgp_reset_image(0); } } - // draw player - if (e->is_player && - get_entity(&gs, e->currently_inside_of_box) == NULL) + // gold target + set_color(GOLD); + draw_filled_rect(gs.goldpos.x, gs.goldpos.y, 0.1, 0.1); + + // instant death + set_color(RED); + draw_circle((cpVect){0}, INSTANT_DEATH_DISTANCE_FROM_CENTER); + + // the SUN + SUNS_ITER(&gs) { transform_scope { - rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); - set_color_values(1.0, 1.0, 1.0, 1.0); + translate(entity_pos(i.sun).x, entity_pos(i.sun).y); + set_color(WHITE); + sgp_set_image(0, image_sun); + draw_texture_centered((cpVect){0}, i.sun->sun_radius * 2.0); + sgp_reset_image(0); - pipeline_scope(hueshift_pipeline) - { - setup_hueshift(e->owning_squad); - sgp_set_image(0, image_player); - draw_texture_rectangle_centered( - entity_pos(e), cpvmult(PLAYER_SIZE, player_scaling)); - sgp_reset_image(0); - } + // can draw at 0,0 because everything relative to sun now! + + // sun DEATH RADIUS + + set_color(BLUE); + draw_circle((cpVect){0}, sun_dist_no_gravity(i.sun)); } } - if (e->is_explosion) - { - sgp_set_image(0, image_explosion); - set_color_values(1.0, 1.0, 1.0, - 1.0 - (e->explosion_progress / EXPLOSION_TIME)); - draw_texture_centered(e->explosion_pos, e->explosion_radius * 2.0); - sgp_reset_image(0); - } - } - // gold target - set_color(GOLD); - draw_filled_rect(gs.goldpos.x, gs.goldpos.y, 0.1, 0.1); - - // instant death - set_color(RED); - draw_circle((cpVect){0}, INSTANT_DEATH_DISTANCE_FROM_CENTER); + set_color_values(1.0, 1.0, 1.0, 1.0); + dbg_drawall(); + } // world space transform end - // the SUN - SUNS_ITER(&gs) + // low health + if (myentity() != NULL) { - transform_scope - { - translate(entity_pos(i.sun).x, entity_pos(i.sun).y); - set_color(WHITE); - sgp_set_image(0, image_sun); - draw_texture_centered((cpVect){0}, i.sun->sun_radius * 2.0); - sgp_reset_image(0); - - // can draw at 0,0 because everything relative to sun now! - - // sun DEATH RADIUS - - set_color(BLUE); - draw_circle((cpVect){0}, sun_dist_no_gravity(i.sun)); - } + set_color_values(1.0, 1.0, 1.0, myentity()->damage); + sgp_set_image(0, image_low_health); + draw_texture_rectangle_centered((cpVect){width / 2.0, height / 2.0}, + (cpVect){width, height}); + sgp_reset_image(0); } - set_color_values(1.0, 1.0, 1.0, 1.0); - dbg_drawall(); - } // world space transform end - - // low health - if (myentity() != NULL) - { - set_color_values(1.0, 1.0, 1.0, myentity()->damage); - sgp_set_image(0, image_low_health); - draw_texture_rectangle_centered((cpVect){width / 2.0, height / 2.0}, - (cpVect){width, height}); - sgp_reset_image(0); + // UI drawn in screen space + ui(true, dt, width, height); } - // 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(); } - - 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) @@ -2130,6 +2155,9 @@ void cleanup(void) ma_mutex_unlock(&server_info.info_mutex); WaitForSingleObject(server_thread_handle, INFINITE); + end_profiling_mythread(); + end_profiling(); + ma_mutex_uninit(&send_packets_mutex); ma_mutex_uninit(&play_packets_mutex); @@ -2248,6 +2276,8 @@ sapp_desc sokol_main(int argc, char *argv[]) stm_setup(); ma_mutex_init(&server_info.info_mutex); server_info.world_save = "debug_world.bin"; + init_profiling("astris.spall"); + init_profiling_mythread(0); if (argc > 1) { server_thread_handle = diff --git a/profiling.h b/profiling.h new file mode 100644 index 0000000..8f692ca --- /dev/null +++ b/profiling.h @@ -0,0 +1,98 @@ +#include "types.h" +#include // malloc the profiling buffer + +#ifdef PROFILING_H +#error only include profiling.h once +#endif +#define PROFILING_H + +#ifdef PROFILING +#define PROFILING_BUFFER_SIZE (1 * 1024 * 1024) + +#ifdef PROFILING_IMPL +#define SPALL_IMPLEMENTATION +#pragma warning(disable : 4996) // spall uses fopen +#include "spall.h" + +#define WIN32_LEAN_AND_MEAN +#define VC_EXTRALEAN +#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; +} +SpallProfile spall_ctx; +THREADLOCAL SpallBuffer spall_buffer; +THREADLOCAL unsigned char *buffer_data = NULL; +THREADLOCAL uint32_t my_thread_id = 0; + +void init_profiling(const char *filename) +{ + spall_ctx = SpallInit(filename, 1); +} + +void init_profiling_mythread(uint32_t id) +{ + my_thread_id = id; + if (buffer_data != NULL) + { + *(int *)0 = 0; + } + buffer_data = malloc(PROFILING_BUFFER_SIZE); + spall_buffer = (SpallBuffer){ + .length = PROFILING_BUFFER_SIZE, + .data = buffer_data, + }; + SpallBufferInit(&spall_ctx, &spall_buffer); +} + +void end_profiling_mythread() +{ + SpallBufferFlush(&spall_ctx, &spall_buffer); + SpallBufferQuit(&spall_ctx, &spall_buffer); + free(buffer_data); +} + +void end_profiling() +{ + SpallQuit(&spall_ctx); +} + +#endif // PROFILING_IMPL + +#include "spall.h" + +extern SpallProfile spall_ctx; +extern THREADLOCAL SpallBuffer spall_buffer; +extern THREADLOCAL uint32_t my_thread_id; + +double get_time_in_micros(); +void init_profiling(const char *filename); +// you can pass anything to id as long as it's different from other threads +void init_profiling_mythread(uint32_t id); +void end_profiling(); +void end_profiling_mythread(); + +#define PROFILE_SCOPE(name) DeferLoop(SpallTraceBeginLenTidPid(&spall_ctx, &spall_buffer, name, sizeof(name) - 1, my_thread_id, 0, get_time_in_micros()), SpallTraceEndTidPid(&spall_ctx, &spall_buffer, my_thread_id, 0, get_time_in_micros())) + +#else // PROFILING + +void inline init_profiling(const char *filename) { (void)filename; } +// you can pass anything to id as long as it's different from other threads +void inline init_profiling_mythread(uint32_t id) { (void)id; } +void inline end_profiling() {} +void inline end_profiling_mythread() {} + +#define PROFILE_SCOPE(name) +#endif diff --git a/server.c b/server.c index d8818d8..da8b945 100644 --- a/server.c +++ b/server.c @@ -1,5 +1,5 @@ +#include "chipmunk.h" // initializing bodies #include "sokol_time.h" -#include // initializing bodies #include "types.h" #include #include @@ -20,40 +20,7 @@ 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 -#include "spall.h" - -#define WIN32_LEAN_AND_MEAN -#define VC_EXTRALEAN -#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; -} - -static SpallProfile spall_ctx; -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 - -#define PROFILE_SCOPE(name) - -#endif +#include "profiling.h" // started in a thread from host void server(void *info_raw) @@ -61,14 +28,6 @@ void server(void *info_raw) 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); #endif @@ -79,8 +38,10 @@ void server(void *info_raw) gs.server_side_computing = true; Log("Allocated %zu bytes for entities\n", entities_size); + init_profiling_mythread(1); + create_initial_world(&gs); - + // inputs Queue player_input_queues[MAX_PLAYERS] = {0}; size_t input_queue_data_size = QUEUE_SIZE_FOR_ELEMENTS(sizeof(InputFrame), INPUT_QUEUE_MAX); @@ -95,9 +56,9 @@ void server(void *info_raw) OpusEncoder *player_encoders[MAX_PLAYERS] = {0}; OpusDecoder *player_decoders[MAX_PLAYERS] = {0}; - #ifdef DEBUG_WORLD +#ifdef DEBUG_WORLD world_save_name = NULL; - #endif +#endif if (world_save_name != NULL) { size_t read_game_data_buffer_size = entities_size; @@ -199,333 +160,336 @@ void server(void *info_raw) double audio_time_to_send = 0.0; double total_time = 0.0; unsigned char *world_save_buffer = malloc(entities_size); - while (true) + PROFILE_SCOPE("Serving") { - 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) + ma_mutex_lock(&info->info_mutex); + if (info->should_quit) { - fprintf(stderr, "Enet host service error %d\n", ret); + ma_mutex_unlock(&info->info_mutex); + break; } - if (ret > 0) + ma_mutex_unlock(&info->info_mutex); + + // @Speed handle enet messages and simulate gamestate in parallel, then sync... must clone gamestate for this + while (true) { - switch (event.type) + int ret = enet_host_service(enet_host, &event, 0); + if (ret == 0) + break; + if (ret < 0) { - case ENET_EVENT_TYPE_CONNECT: + fprintf(stderr, "Enet host service error %d\n", ret); + } + if (ret > 0) { - 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++) + switch (event.type) { - if (!gs.players[i].connected) + 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++) { - player_slot = i; - break; + 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; - create_player(&gs.players[player_slot]); - - 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); + 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; + create_player(&gs.players[player_slot]); + + 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); + } } - } - break; + 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) + case ENET_EVENT_TYPE_RECEIVE: { - Log("Wtf an empty packet from enet?\n"); - } - else - { - int64_t player_slot = (int64_t)event.peer->data; - size_t length = event.packet->dataLength; + // 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; + 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]}; - unsigned char decompressed[MAX_CLIENT_TO_SERVER] = {0}; - size_t decompressed_max_len = MAX_CLIENT_TO_SERVER; - assert(LZO1X_MEM_DECOMPRESS == 0); + queue_clear(&player_input_queues[player_slot]); + struct ClientToServer received = {.mic_data = buffer_to_fill, .input_data = &player_input_queues[player_slot]}; + unsigned 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); + 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)) + 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("Bad packet from client %d\n", (int)player_slot); + Log("Couldn't decompress player packet, error code %d from lzo\n", return_value); } } - else + /* 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) { - Log("Couldn't decompress player packet, error code %d from lzo\n", return_value); + 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; } - /* Clean up the packet now that we're done using it. */ - enet_packet_destroy(event.packet); - } - break; + 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) + case ENET_EVENT_TYPE_NONE: { - 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; + 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 double max_time = 5.0 * TIMESTEP; - if (total_time > max_time) - { - Log("Abnormally large total time %f, clamping\n", total_time); - total_time = max_time; - } + 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 double max_time = 5.0 * 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) + PROFILE_SCOPE("World Processing") { - int this_player_index = (int)(int64_t)cur->data; - QUEUE_ITER(&player_input_queues[this_player_index], cur_header) + CONNECTED_PEERS(enet_host, cur) { - InputFrame *cur = (InputFrame *)cur_header->data; - if (cur->tick == tick(&gs)) + int this_player_index = (int)(int64_t)cur->data; + QUEUE_ITER(&player_input_queues[this_player_index], cur_header) { - gs.players[this_player_index].input = *cur; - break; + 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; + process(&gs, TIMESTEP, false); + 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") + if (world_save_name != NULL && (stm_sec(stm_diff(stm_now(), last_saved_world_time))) > TIME_BETWEEN_WORLD_SAVE) { - 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)) + PROFILE_SCOPE("Save World") { - 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 + 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)) { - size_t data_written = fwrite(world_save_buffer, sizeof(*world_save_buffer), out_len, save_file); - if (data_written != out_len) + FILE *save_file = NULL; + fopen_s(&save_file, (const char *)world_save_name, "wb"); + if (save_file == NULL) { - Log("Failed to save world data, wanted to write %zu but could only write %zu\n", out_len, data_written); + Log("Could not open save file: errno %d\n", errno); } else { - Log("Saved game world to %s\n", (const char *)world_save_name); + 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); } - fclose(save_file); } - } - else - { - Log("URGENT: FAILED TO SAVE WORLD FILE!\n"); + 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") + if (stm_sec(stm_diff(stm_now(), last_sent_gamestate_time)) > TIME_BETWEEN_SEND_GAMESTATE) { - static char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = {0}; + 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.0 / (VOIP_TIME_PER_PACKET / audio_time_to_send)); + 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.0 / (VOIP_TIME_PER_PACKET / audio_time_to_send)); #define MAX_AUDIO_PACKETS_TO_SEND 12 - 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; - } + 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}; + 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; + 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++) + // decode what everybody said + CONNECTED_PEERS(enet_host, cur) { - 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); + 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 - unsigned char *bytes_buffer = malloc(sizeof *bytes_buffer * MAX_SERVER_TO_CLIENT); - unsigned 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); + // send gamestate to each player + CONNECTED_PEERS(enet_host, cur) { - for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) + 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 + unsigned char *bytes_buffer = malloc(sizeof *bytes_buffer * MAX_SERVER_TO_CLIENT); + unsigned 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); { - 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) + for (int packet_i = 0; packet_i < num_audio_packets; packet_i++) { - if (other_player != cur) + 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) { - 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) + if (other_player != cur) { - double dist = cpvdist(entity_pos(this_player_entity), entity_pos(other_player_entity)); - double volume = lerp(1.0, 0.0, clamp01(dist / VOIP_DISTANCE_WHEN_CANT_HEAR)); - if (volume > 0.01) + 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) { - for (int frame_i = 0; frame_i < VOIP_EXPECTED_FRAME_COUNT; frame_i++) + double dist = cpvdist(entity_pos(this_player_entity), entity_pos(other_player_entity)); + double volume = lerp(1.0, 0.0, clamp01(dist / VOIP_DISTANCE_WHEN_CANT_HEAR)); + if (volume > 0.01) { - to_send_to_cur[frame_i] += (opus_int16)((float)decoded_audio_packets[other_player_index][packet_i][frame_i] * volume); + 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; + 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) + 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)) { - Log("Too much data quitting!\n"); - exit(-1); - } + 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); + 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) + 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("Enet failed to send packet error %d\n", err); - enet_packet_destroy(gamestate_packet); + Log("Failed to serialize data for client %d\n", this_player_index); } + free(bytes_buffer); + free(compressed_buffer); } - else - { - Log("Failed to serialize data for client %d\n", this_player_index); - } - free(bytes_buffer); - free(compressed_buffer); } } } @@ -547,10 +511,9 @@ void server(void *info_raw) enet_host_destroy(enet_host); enet_deinitialize(); + end_profiling_mythread(); printf("Cleanup\n"); #ifdef PROFILING - SpallBufferQuit(&spall_ctx, &spall_buffer); - SpallQuit(&spall_ctx); #endif } \ No newline at end of file diff --git a/server.spall b/server.spall deleted file mode 100644 index 19d342b..0000000 Binary files a/server.spall and /dev/null differ diff --git a/types.h b/types.h index c677054..f565583 100644 --- a/types.h +++ b/types.h @@ -337,7 +337,9 @@ typedef struct GameState // @Robust for the integer tick, also store a double for how much time has been processed. // Like a whole timestep then a double for subtimestep - double time; // @Robust separate tick integer not prone to precision issues. Could be very large as is saved to disk! + uint64_t tick; + double subframe_time; + cpVect goldpos; @@ -421,9 +423,12 @@ void create_initial_world(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, double dt); // does in place +// if is subframe, doesn't always increment the tick. When enough +// subframe time has been processed, increments the tick +void process(struct GameState *gs, double dt, bool is_subframe); // does in place Entity *closest_box_to_point_in_radius(struct GameState *gs, cpVect point, double radius, bool (*filter_func)(Entity *)); uint64_t tick(struct GameState *gs); +double time(GameState *gs); double sun_dist_no_gravity(Entity *sun); // all of these return if successful or not