#include <chipmunk.h>
#define QUEUE_IMPL
#include "queue.h"
#include "stdbool.h"
#include "types.h"
#define PROFILING_IMPL
#include "profiling.h"

#include "buildsettings.h" // debug/developer settings

#include <stdio.h>  // assert logging
#include <string.h> // memset

// do not use any global variables to process gamestate

// super try not to depend on external libraries like enet or sokol to keep build process simple,
// gamestate its own portable submodule. If need to link to other stuff document here:
// - debug.c for debug drawing
// - chipmunk

enum
{
  PLAYERS = 1 << 0,
  BOXES = 1 << 1,
};

void __flight_assert(bool cond, const char *file, int line, const char *cond_string)
{
  if (!cond)
  {
    fprintf(stderr, "%s:%d | Assertion %s failed\n", file, line, cond_string);
  }
}

bool is_burning(Entity *missile)
{
  assert(missile->is_missile);
  return missile->time_burned_for < MISSILE_BURN_TIME;
}

bool was_entity_deleted(GameState *gs, EntityID id)
{
  if (id.generation == 0)
    return false; // generation 0 means null entity ID, not a deleted entity
  Entity *the_entity = &gs->entities[id.index];
  return (!the_entity->exists || the_entity->generation != id.generation);
}

Entity *get_entity_even_if_dead(GameState *gs, EntityID id)
{
  if (id.generation == 0)
  {
    return NULL;
  }
  if (!(id.index < gs->cur_next_entity || gs->cur_next_entity == 0))
    return NULL;
  if (!(id.index < gs->max_entities))
    return NULL;
  Entity *to_return = &gs->entities[id.index];
  // don't validate the generation either
  return to_return;
}

// may return null if it doesn't exist anymore
Entity *get_entity(GameState *gs, EntityID id)
{

  Entity *to_return = get_entity_even_if_dead(gs, id);
  if (was_entity_deleted(gs, id))
    return NULL;
  return to_return;
}

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 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)
{
  assert(this_players_perspective != NULL);
  assert(this_players_perspective->is_player);
  bool cloaked = cloaking_active(gs, e);
  if (e->is_player)
  {
    return cloaked && e->owning_squad != this_players_perspective->owning_squad;
  }
  else
  {
    return cloaked && this_players_perspective->owning_squad != e->last_cloaked_by_squad;
  }
}

static BOX_UNLOCKS_TYPE box_unlock_number(enum BoxType box)
{
  assert((BOX_UNLOCKS_TYPE)box < 64);
  return (BOX_UNLOCKS_TYPE)((BOX_UNLOCKS_TYPE)1 << ((BOX_UNLOCKS_TYPE)box));
}

static bool learned_boxes_has_box(BOX_UNLOCKS_TYPE learned, enum BoxType box)
{
  return (learned & box_unlock_number(box)) > 0;
}

void unlock_box(Player *player, enum BoxType box)
{
  assert(box < MAX_BOX_TYPES);
  assert(box != BoxInvalid);
  player->box_unlocks |= box_unlock_number(box);
}

bool box_unlocked(Player *player, enum BoxType box)
{
  assert(box < MAX_BOX_TYPES);
  if (box == BoxInvalid)
    return false;
  return learned_boxes_has_box(player->box_unlocks, box);
}

EntityID get_id(GameState *gs, Entity *e)
{
  if (e == NULL)
    return (EntityID){0};

  size_t index = (e - gs->entities);
  assert(index >= 0);
  assert(index < gs->cur_next_entity);

  return (EntityID){
      .generation = e->generation,
      .index = (unsigned int)index,
  };
}

static Entity *cp_shape_entity(cpShape *shape)
{
  return (Entity *)cpShapeGetUserData(shape);
}

static Entity *cp_body_entity(cpBody *body)
{
  return (Entity *)cpBodyGetUserData(body);
}

static GameState *cp_space_gs(cpSpace *space)
{
  return (GameState *)cpSpaceGetUserData(space);
}

static GameState *entitys_gamestate(Entity *e)
{
  assert(e->body != NULL || e->shape != NULL);
  if (e->shape != NULL)
  {
    return cp_space_gs(cpShapeGetSpace(e->shape));
  }
  if (e->body != NULL)
  {
    return cp_space_gs(cpBodyGetSpace(e->body));
  }
  return NULL;
}

int grid_num_boxes(GameState *gs, Entity *e)
{
  assert(e->is_grid);
  int to_return = 0;

  BOXES_ITER(gs, cur, e)
  to_return++;

  return to_return;
}

void box_remove_from_boxes(GameState *gs, Entity *box)
{
  assert(box->is_box);
  Entity *prev_box = get_entity(gs, box->prev_box);
  Entity *next_box = get_entity(gs, box->next_box);
  if (prev_box != NULL)
  {
    if (prev_box->is_box)
      prev_box->next_box = get_id(gs, next_box);
    else if (prev_box->is_grid)
      prev_box->boxes = get_id(gs, next_box);
  }
  if (next_box != NULL)
  {
    assert(next_box->is_box);
    next_box->prev_box = get_id(gs, prev_box);
  }
  box->next_box = (EntityID){0};
  box->prev_box = (EntityID){0};
}

cpVect player_vel(GameState *gs, Entity *e);
cpVect entity_vel(GameState *gs, Entity *e)
{
  assert(e->is_box || e->is_player || e->body != NULL || e->is_explosion);
  if (e->is_box)
    return box_vel(e);
  if (e->is_player)
    return player_vel(gs, e);
  if (e->body != NULL)
    return (cpBodyGetVelocity(e->body));
  if (e->is_explosion)
    return e->explosion_vel;
  assert(false);
  return (cpVect){0};
}

static THREADLOCAL double to_face = 0.0;
static THREADLOCAL double nearest_dist = INFINITY;
static THREADLOCAL bool target_found = false;
static void on_missile_shape(cpShape *shape, cpContactPointSet *points, void *data)
{
  Entity *launcher = (Entity *)data;
  Entity *other = cp_shape_entity(shape);
  GameState *gs = entitys_gamestate(launcher);
  assert(other->is_box || other->is_player || other->is_missile);

  cpVect to = cpvsub(entity_pos(other), entity_pos(launcher));
  bool should_attack = true;
  if (other->is_box && box_grid(other) == box_grid(launcher))
    should_attack = false;
  if (other->owning_squad == launcher->owning_squad)
    should_attack = false;

  if (should_attack && cpvlength(to) < nearest_dist)
  {
    target_found = true;
    nearest_dist = cpvlength(to);

    // lookahead by their velocity
    cpVect rel_velocity = cpvsub(entity_vel(gs, other), entity_vel(gs, launcher));
    double dist = cpvdist(entity_pos(other), entity_pos(launcher));

    double time_of_travel = sqrt((2.0 * dist) / (MISSILE_BURN_FORCE / MISSILE_MASS));

    cpVect other_future_pos = cpvadd(entity_pos(other), cpvmult(rel_velocity, time_of_travel));

    cpVect adjusted_to = cpvsub(other_future_pos, entity_pos(launcher));

    to_face = cpvangle(adjusted_to);
  }
}

LauncherTarget missile_launcher_target(GameState *gs, Entity *launcher)
{
  to_face = 0.0;
  cpBody *tmp = cpBodyNew(0.0, 0.0);
  cpBodySetPosition(tmp, (entity_pos(launcher)));
  cpShape *circle = cpCircleShapeNew(tmp, MISSILE_RANGE, cpv(0, 0));

  nearest_dist = INFINITY;
  to_face = 0.0;
  target_found = false;
  cpSpaceShapeQuery(gs->space, circle, on_missile_shape, (void *)launcher);

  cpBodyFree(tmp);
  cpShapeFree(circle);
  return (LauncherTarget){.target_found = target_found, .facing_angle = to_face};
}

void on_entity_child_shape(cpBody *body, cpShape *shape, void *data);

// gs is for iterating over all child shapes and destroying those, too
static void destroy_body(GameState *gs, cpBody **body)
{
  if (*body != NULL)
  {
    cpBodyEachShape(*body, on_entity_child_shape, (void *)gs);
    cpSpaceRemoveBody(gs->space, *body);
    cpBodyFree(*body);
    *body = NULL;
  }
  *body = NULL;
}

void entity_destroy(GameState *gs, Entity *e)
{
  assert(e->exists);

  if (e->is_grid)
  {
    BOXES_ITER(gs, cur, e)
    entity_destroy(gs, cur);
  }
  if (e->is_box)
  {
    box_remove_from_boxes(gs, e);
  }

  if (e->shape != NULL)
  {
    cpSpaceRemoveShape(gs->space, e->shape);
    cpShapeFree(e->shape);
    e->shape = NULL;
  }
  destroy_body(gs, &e->body);

  Entity *front_of_free_list = get_entity(gs, gs->free_list);
  if (front_of_free_list != NULL)
    assert(!front_of_free_list->exists);
  int gen = e->generation;
  *e = (Entity){0};
  e->generation = gen;
  e->next_free_entity = gs->free_list;
  gs->free_list = get_id(gs, e);
}

void on_entity_child_shape(cpBody *body, cpShape *shape, void *data)
{
  entity_destroy((GameState *)data, cp_shape_entity(shape));
}

Entity *new_entity(GameState *gs)
{
  Entity *to_return = NULL;
  Entity *possible_free_list = get_entity_even_if_dead(gs, gs->free_list);
  if (possible_free_list != NULL)
  {
    assert(possible_free_list->generation == gs->free_list.generation);
    to_return = possible_free_list;
    assert(!to_return->exists);
    gs->free_list = to_return->next_free_entity;
  }
  else
  {
    assert(gs->cur_next_entity < gs->max_entities); // too many entities if fails
    to_return = &gs->entities[gs->cur_next_entity];
    gs->cur_next_entity++;
  }

  to_return->generation++;
  to_return->exists = true;
  return to_return;
}

// pos, mass, radius
EntityID create_sun(GameState *gs, Entity *new_sun, cpVect pos, cpVect vel, double mass, double radius)
{
  assert(new_sun != NULL);
  new_sun->is_sun = true;
  new_sun->sun_pos = pos;
  new_sun->sun_vel = vel;
  new_sun->sun_mass = mass;
  new_sun->sun_radius = radius;

  return get_id(gs, new_sun);
}

void create_body(GameState *gs, Entity *e)
{
  assert(gs->space != NULL);

  if (e->body != NULL)
  {
    cpSpaceRemoveBody(gs->space, e->body);
    cpBodyFree(e->body);
    e->body = NULL;
  }

  cpBody *body = cpSpaceAddBody(gs->space, cpBodyNew(0.0, 0.0)); // zeros for mass/moment of inertia means automatically calculated from its collision shapes
  e->body = body;
  cpBodySetUserData(e->body, (void *)e);
}

cpVect player_vel(GameState *gs, Entity *player)
{
  assert(player->is_player);
  Entity *potential_seat = get_entity(gs, player->currently_inside_of_box);
  if (potential_seat != NULL && !potential_seat->is_box)
  {
    Log("Weird ass motherfucking bug where the seat inside of is an explosion or some shit\n");
    assert(potential_seat->is_box);
  }
  else
  {
    if (potential_seat != NULL)
    {
      return (cpBodyGetVelocity(get_entity(gs, potential_seat->shape_parent_entity)->body));
    }
  }
  return (cpBodyGetVelocity(player->body));
}

void grid_create(GameState *gs, Entity *e)
{
  e->is_grid = true;
  create_body(gs, e);
}

void entity_set_rotation(Entity *e, double rot)
{
  assert(e->body != NULL);
  cpBodySetAngle(e->body, rot);
}

void entity_set_pos(Entity *e, cpVect pos)
{
  assert(e->is_grid);
  assert(e->body != NULL);
  cpBodySetPosition(e->body, (pos));
}

// size is (1/2 the width, 1/2 the height)
void create_rectangle_shape(GameState *gs, Entity *e, Entity *parent, cpVect pos, cpVect size, double mass)
{
  if (e->shape != NULL)
  {
    cpSpaceRemoveShape(gs->space, e->shape);
    cpShapeFree(e->shape);
    e->shape = NULL;
  }

  cpBB box = cpBBNew(-size.x + pos.x, -size.y + pos.y, size.x + pos.x, size.y + pos.y);
  cpVect verts[4] = {
      cpv(box.r, box.b),
      cpv(box.r, box.t),
      cpv(box.l, box.t),
      cpv(box.l, box.b),
  };

  e->shape_size = size;
  e->shape_parent_entity = get_id(gs, parent);
  e->shape = (cpShape *)cpPolyShapeInitRaw(cpPolyShapeAlloc(), parent->body, 4, verts, 0.0); // this cast is done in chipmunk, not sure why it works
  cpShapeSetUserData(e->shape, (void *)e);
  cpShapeSetMass(e->shape, mass);
  cpSpaceAddShape(gs->space, e->shape);
}

#define PLAYER_SHAPE_FILTER cpShapeFilterNew(CP_NO_GROUP, PLAYERS, CP_ALL_CATEGORIES)

void create_player(Player *player)
{
  // default box unlocks, required for survival and growth
#ifdef UNLOCK_ALL
  for (enum BoxType t = BoxInvalid + 1; t < BoxLast; t++)
    unlock_box(player, t);
#else
  unlock_box(player, BoxHullpiece);
  unlock_box(player, BoxThruster);
  unlock_box(player, BoxBattery);
  unlock_box(player, BoxCockpit);
  unlock_box(player, BoxMedbay);
  unlock_box(player, BoxSolarPanel);
  unlock_box(player, BoxScanner);
#endif
}

void create_missile(GameState *gs, Entity *e)
{
  create_body(gs, e);
  create_rectangle_shape(gs, e, e, (cpVect){0}, cpvmult(MISSILE_COLLIDER_SIZE, 0.5), PLAYER_MASS);
  e->is_missile = true;
}

void create_player_entity(GameState *gs, Entity *e)
{
  e->is_player = true;
  e->no_save_to_disk = true;
  create_body(gs, e);
  create_rectangle_shape(gs, e, e, (cpVect){0}, cpvmult(PLAYER_SIZE, 0.5), PLAYER_MASS);
  cpShapeSetFilter(e->shape, PLAYER_SHAPE_FILTER);
}

void box_add_to_boxes(GameState *gs, Entity *grid, Entity *box_to_add)
{
  box_to_add->next_box = get_id(gs, get_entity(gs, grid->boxes));
  box_to_add->prev_box = get_id(gs, grid);
  if (get_entity(gs, box_to_add->next_box) != NULL)
  {
    get_entity(gs, box_to_add->next_box)->prev_box = get_id(gs, box_to_add);
  }
  grid->boxes = get_id(gs, box_to_add);
}

// box must be passed as a parameter as the box added to chipmunk uses this pointer in its
// user data. pos is in local coordinates. Adds the box to the grid's chain of boxes
void box_create(GameState *gs, Entity *new_box, Entity *grid, cpVect pos)
{
  new_box->is_box = true;
  assert(gs->space != NULL);
  assert(grid->is_grid);

  double halfbox = BOX_SIZE / 2.0;

  create_rectangle_shape(gs, new_box, grid, pos, (cpVect){halfbox, halfbox}, 1.0);

  cpShapeSetFilter(new_box->shape, cpShapeFilterNew(CP_NO_GROUP, BOXES, CP_ALL_CATEGORIES));

  box_add_to_boxes(gs, grid, new_box);
}

cpVect box_compass_vector(Entity *box)
{

  assert(box->is_box);
  cpVect to_return = (cpVect){.x = 1.0, .y = 0.0};
  to_return = cpvspin(to_return, rotangle(box->compass_rotation));

  return to_return;
}

// removes boxes from grid, then ensures that the rule that grids must not have
// holes in them is applied.
static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
{
  int num_boxes = grid_num_boxes(gs, grid);
  if (num_boxes == 0)
  {
    entity_destroy(gs, grid);
    return;
  }
  if (num_boxes == 1)
    return;

    // could be a gap between boxes in the grid, separate into multiple grids

    // goal: create list of "real grids" from this grid that have boxes which are
    // ONLY connected horizontally and vertically. whichever one of these "real grids"
    // has the most blocks stays the current grid, so
    // if a player is inhabiting this ship it stays that ship.
    // The other "real grids" are allocated as new grids

#define MAX_SEPARATE_GRIDS 8
  EntityID separate_grids[MAX_SEPARATE_GRIDS] = {0};
  int cur_separate_grid_index = 0;
  int cur_separate_grid_size = 0;
  int processed_boxes = 0;

  int biggest_separate_grid_index = 0;
  int biggest_separate_grid_length = 0;

  // process all boxes into separate, but correctly connected, grids
  while (processed_boxes < num_boxes)
  {
    // grab an unprocessed box, one not in separate_grids, to start the flood fill
    Entity *unprocessed = get_entity(gs, grid->boxes);
    assert(unprocessed != NULL);
    assert(unprocessed->is_box);
    box_remove_from_boxes(gs, unprocessed); // no longer in the boxes list of the grid

    // flood fill from this unprocessed box, adding each result to cur_separate_grid_index,
    // removing each block from the grid
    // https://en.wikipedia.org/wiki/Flood_fill
    {
      // queue stuff @Robust use factored datastructure
      EntityID Q = get_id(gs, unprocessed);
      Entity *N = NULL;
      while (true)
      {
        assert(!was_entity_deleted(gs, Q));
        N = get_entity(gs, Q);
        if (N == NULL) // must mean that the queue is empty
          break;
        Q = N->next_box;
        if (true) // if node "inside", this is always true
        {
          N->next_box = separate_grids[cur_separate_grid_index];
          separate_grids[cur_separate_grid_index] = get_id(gs, N);
          cur_separate_grid_size++;
          processed_boxes++;

          cpVect cur_local_pos = entity_shape_pos(N);
          const cpVect dirs[] = {
              (cpVect){
                  .x = -1.0, .y = 0.0},
              (cpVect){
                  .x = 1.0, .y = 0.0},
              (cpVect){
                  .x = 0.0, .y = 1.0},
              (cpVect){
                  .x = 0.0, .y = -1.0},
          };
          int num_dirs = sizeof(dirs) / sizeof(*dirs);

          for (int ii = 0; ii < num_dirs; ii++)
          {
            cpVect dir = dirs[ii];
            EntityID box_in_direction = (EntityID){0};
            // @Robust @Speed faster method, not O(N^2), of getting the box
            // in the direction currently needed
            cpVect compass_vect = box_compass_vector(N);
            if (N->box_type == BoxMerge && N->wants_disconnect && cpvnear(compass_vect, dir, 0.01))
            {
            }
            else
            {
              cpVect wanted_local_pos = cpvadd(cur_local_pos, cpvmult(dir, BOX_SIZE));
              BOXES_ITER(gs, cur, grid)
              {
                if (cpvnear(entity_shape_pos(cur), wanted_local_pos, 0.01))
                {
                  box_in_direction = get_id(gs, cur);
                  break;
                }
              }
            }

            Entity *newbox = get_entity(gs, box_in_direction);

            if (newbox != NULL && newbox->box_type == BoxMerge && newbox->wants_disconnect && cpvnear(cpvmult(box_compass_vector(newbox), -1.0), dir, 0.01))
            {
              newbox = NULL;
            }

            if (newbox != NULL)
            {
              box_remove_from_boxes(gs, newbox);
              newbox->next_box = Q;
              Q = box_in_direction;
            }
          }
        }
      }
    }

    if (cur_separate_grid_size > biggest_separate_grid_length)
    {
      biggest_separate_grid_length = cur_separate_grid_size;
      biggest_separate_grid_index = cur_separate_grid_index;
    }
    cur_separate_grid_index++;
    assert(cur_separate_grid_index < MAX_SEPARATE_GRIDS);
    cur_separate_grid_size = 0;
  }

  // create new grids for all lists of boxes except for the biggest one.
  // delete the boxes out of the current grid as I pull boxes into separate ones
  // which are no longer connected
  for (int sepgrid_i = 0; sepgrid_i < MAX_SEPARATE_GRIDS; sepgrid_i++)
  {
    EntityID cur_separate_grid = separate_grids[sepgrid_i];
    if (get_entity(gs, cur_separate_grid) == NULL)
      continue; // this separate grid is empty

    Entity *new_grid;
    if (sepgrid_i == biggest_separate_grid_index)
    {
      new_grid = grid;
    }
    else
    {
      new_grid = new_entity(gs);
      grid_create(gs, new_grid);
      cpBodySetPosition(new_grid->body, cpBodyGetPosition(grid->body));
      cpBodySetAngle(new_grid->body, cpBodyGetAngle(grid->body));
    }

    Entity *cur = get_entity(gs, cur_separate_grid);
    while (cur != NULL)
    {
      Entity *next = get_entity(gs, cur->next_box);
      box_create(gs, cur, new_grid, entity_shape_pos(cur)); // destroys next/prev fields on cur
      cur = next;
    }

    cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, (grid_com(new_grid))));
    cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(grid));
  }
}

static void grid_remove_box(GameState *gs, struct Entity *grid, struct Entity *box)
{
  assert(grid->is_grid);
  assert(box->is_box);
  entity_destroy(gs, box);
  grid_correct_for_holes(gs, grid);
}

static void on_damage(cpArbiter *arb, cpSpace *space, cpDataPointer userData)
{
  cpShape *a, *b;
  cpArbiterGetShapes(arb, &a, &b);

  Entity *entity_a, *entity_b;
  entity_a = cp_shape_entity(a);
  entity_b = cp_shape_entity(b);

  Entity *potential_missiles[] = {entity_a, entity_b};
  for (Entity **missile_ptr = potential_missiles; missile_ptr - potential_missiles < ARRLEN(potential_missiles); missile_ptr++)
  {
    Entity *missile = entity_a;
    cpVect (*getPointFunc)(const cpArbiter *arb, int i) = NULL;
    if (missile == entity_a)
      getPointFunc = cpArbiterGetPointA;
    if (missile == entity_b)
      getPointFunc = cpArbiterGetPointB;

    if (missile->is_missile)
    {
      int count = cpArbiterGetCount(arb);
      for (int i = 0; i < count; i++)
      {
        cpVect collision_point = getPointFunc(arb, i);
        cpVect local_collision_point = (cpBodyWorldToLocal(missile->body, collision_point));
        if (local_collision_point.x > MISSILE_COLLIDER_SIZE.x * 0.2)
        {
          missile->damage += MISSILE_DAMAGE_THRESHOLD * 2.0;
        }
      }
    }
  }

  // if(entity_a->is_missile) {getPointFunc = cpArbiterGetPointA;
  // if(entity_b->is_missile) getPointFunc = cpArbiterGetPointB;

  double damage = cpvlength((cpArbiterTotalImpulse(arb))) * COLLISION_DAMAGE_SCALING;

  if (entity_a->is_box && entity_a->box_type == BoxExplosive)
    entity_a->damage += 2.0 * EXPLOSION_DAMAGE_THRESHOLD;
  if (entity_b->is_box && entity_b->box_type == BoxExplosive)
    entity_b->damage += 2.0 * EXPLOSION_DAMAGE_THRESHOLD;

  if (damage > 0.05)
  {
    // Log("Collision with damage %f\n", damage);
    entity_a->damage += damage;
    entity_b->damage += damage;
  }

  // b must be the key passed into the post step removed, the key is cast into its shape
  // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, b, NULL);
  // cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, a, NULL);
}

// must be called with zero initialized game state, because copies the server side computing!
void initialize(GameState *gs, void *entity_arena, size_t entity_arena_size)
{
  bool is_server_side = gs->server_side_computing;
  *gs = (GameState){0};
  memset(entity_arena, 0, entity_arena_size); // SUPER critical. Random vals in the entity data causes big problem
  gs->entities = (Entity *)entity_arena;
  gs->max_entities = (unsigned int)(entity_arena_size / sizeof(Entity));
  gs->space = cpSpaceNew();
  cpSpaceSetUserData(gs->space, (cpDataPointer)gs);                          // needed in the handler
  cpCollisionHandler *handler = cpSpaceAddCollisionHandler(gs->space, 0, 0); // @Robust limit collision type to just blocks that can be damaged
  handler->postSolveFunc = on_damage;
  gs->server_side_computing = is_server_side;
}
void destroy(GameState *gs)
{
  // can't zero out gs data because the entity memory arena is reused
  // on deserialization
  for (size_t i = 0; i < gs->max_entities; i++)
  {
    if (gs->entities[i].exists)
      entity_destroy(gs, &gs->entities[i]);
  }
  cpSpaceFree(gs->space);
  gs->space = NULL;

  for (size_t i = 0; i < gs->cur_next_entity; i++)
  {
    if (gs->entities[i].exists)
      gs->entities[i] = (Entity){0};
  }
  gs->cur_next_entity = 0;
}
// center of mass, not the literal position
cpVect grid_com(Entity *grid)
{
  return (cpBodyLocalToWorld(grid->body, cpBodyGetCenterOfGravity(grid->body)));
}

cpVect grid_vel(Entity *grid)
{
  return (cpBodyGetVelocity(grid->body));
}
cpVect grid_world_to_local(Entity *grid, cpVect world)
{
  return (cpBodyWorldToLocal(grid->body, (world)));
}
cpVect grid_local_to_world(Entity *grid, cpVect local)
{
  assert(grid->is_grid);
  return (cpBodyLocalToWorld(grid->body, (local)));
}
// returned snapped position is in world coordinates
cpVect grid_snapped_box_pos(Entity *grid, cpVect world)
{
  cpVect local = grid_world_to_local(grid, world);
  local.x /= BOX_SIZE;
  local.y /= BOX_SIZE;
  local.x = round(local.x);
  local.y = round(local.y);
  local.x *= BOX_SIZE;
  local.y *= BOX_SIZE;

  return (cpBodyLocalToWorld(grid->body, (local)));
}

// for boxes does not include box's compass rotation
double entity_rotation(Entity *e)
{
  assert(e->body != NULL || e->shape != NULL);
  if (e->body != NULL)
    return (float)cpBodyGetAngle(e->body);
  else
    return (float)cpBodyGetAngle(cpShapeGetBody(e->shape));
}

double entity_angular_velocity(Entity *grid)
{
  return (float)cpBodyGetAngularVelocity(grid->body);
}
Entity *box_grid(Entity *box)
{
  if (box == NULL)
    return NULL;
  assert(box->is_box);
  return (Entity *)cpBodyGetUserData(cpShapeGetBody(box->shape));
}
// in local space
cpVect entity_shape_pos(Entity *box)
{
  return (cpShapeGetCenterOfGravity(box->shape));
}
double entity_shape_mass(Entity *box)
{
  assert(box->shape != NULL);
  return (float)cpShapeGetMass(box->shape);
}
double box_rotation(Entity *box)
{
  return (float)cpBodyGetAngle(cpShapeGetBody(box->shape));
}

cpVect entity_pos(Entity *e)
{
  if (e->is_box)
  {
    return cpvadd(entity_pos(box_grid(e)), cpvspin(entity_shape_pos(e), entity_rotation(box_grid(e))));
  }
  else if (e->is_explosion)
  {
    return e->explosion_pos;
  }
  else if (e->is_sun)
  {
    return e->sun_pos;
  }
  else
  {
    assert(e->body != NULL);
    return (cpBodyGetPosition(e->body));
  }
}

struct BodyData
{
  cpVect pos;
  cpVect vel;
  double rotation;
  double angular_velocity;
};

void populate(cpBody *body, struct BodyData *data)
{
  data->pos = (cpBodyGetPosition(body));
  data->vel = (cpBodyGetVelocity(body));
  data->rotation = (float)cpBodyGetAngle(body);
  data->angular_velocity = (float)cpBodyGetAngularVelocity(body);
}

void update_from(cpBody *body, struct BodyData *data)
{
  cpBodySetPosition(body, (data->pos));
  cpBodySetVelocity(body, (data->vel));
  cpBodySetAngle(body, data->rotation);
  cpBodySetAngularVelocity(body, data->angular_velocity);
}

typedef struct SerState
{
  unsigned char *bytes;
  bool serializing;
  size_t cursor; // points to next available byte, is the size of current message after serializing something
  size_t max_size;
  Entity *for_player;
  size_t max_entity_index; // for error checking
  bool write_varnames;
  bool save_or_load_from_disk;

  // output
  uint32_t version;
} SerState;

typedef struct SerMaybeFailure
{
  bool failed;
  int line;
  const char *expression;
} SerMaybeFailure;
const static SerMaybeFailure ser_ok = {0};
#define SER_ASSERT(cond)                                                                            \
  if (!(cond))                                                                                      \
  {                                                                                                 \
    __flight_assert(false, __FILE__, __LINE__, #cond);                                              \
    if (ser->save_or_load_from_disk)                                                                \
    {                                                                                               \
      Log("While saving/loading, serialization assertion failed %s on line %d\n", #cond, __LINE__); \
    }                                                                                               \
    else                                                                                            \
    {                                                                                               \
      return (SerMaybeFailure){.failed = true, .line = __LINE__, .expression = #cond};              \
    }                                                                                               \
  }
#define SER_MAYBE_RETURN(maybe_failure)     \
  {                                         \
    SerMaybeFailure result = maybe_failure; \
    if (result.failed)                      \
      return result;                        \
  }
SerMaybeFailure ser_data(SerState *ser, char *data, size_t data_len, const char *name, const char *file, int line)
{
  char var_name[512] = {0};
  size_t var_name_len = 0;
  if (ser->write_varnames)
  {
    snprintf(var_name, 512, "%d%s", line, name); // can't have separator before the name, when comparing names skips past the digit
    var_name_len = strlen(var_name);
  }
  if (ser->serializing)
  {
    if (ser->write_varnames)
    {
      memcpy(ser->bytes + ser->cursor, var_name, var_name_len);
      ser->cursor += var_name_len;
    }
    for (int b = 0; b < data_len; b++)
    {
      ser->bytes[ser->cursor] = data[b];
      ser->cursor += 1;
      SER_ASSERT(ser->cursor < ser->max_size);
    }
  }
  else
  {
    if (ser->write_varnames)
    {
      // deserialize and check the var name

      // skip past the digits
      size_t num_digits = 0;
      while (ser->bytes[ser->cursor] >= '0' && ser->bytes[ser->cursor] <= '9')
      {
        ser->cursor += 1;
        SER_ASSERT(ser->cursor <= ser->max_size);
        num_digits += 1;
        if (num_digits >= 10)
        {
          return (SerMaybeFailure){
              .expression = "Way too many digits as a line number before a field name",
              .failed = true,
              .line = __LINE__,
          };
        }
      }
      // cursor is now on a non digit, the start of the name
      char read_name[512] = {0};
      size_t just_field_name_length = strlen(name);
      for (size_t i = 0; i < just_field_name_length; i++)
      {
        read_name[i] = ser->bytes[ser->cursor];
        ser->cursor += 1;
        SER_ASSERT(ser->cursor <= ser->max_size);
      }

      // now compare!
      SER_ASSERT(strcmp(read_name, name) == 0);
    }
    for (int b = 0; b < data_len; b++)
    {
      data[b] = ser->bytes[ser->cursor];
      ser->cursor += 1;
      SER_ASSERT(ser->cursor <= ser->max_size);
    }
  }
  return ser_ok;
}
SerMaybeFailure ser_var(SerState *ser, char *var_pointer, size_t var_size, const char *name, const char *file, int line)
{
  return ser_data(ser, var_pointer, var_size, name, file, line);
}
#define SER_DATA(data_pointer, data_length) SER_MAYBE_RETURN(ser_data(ser, data_pointer, data_length, #data_pointer, __FILE__, __LINE__))
#define SER_VAR_NAME(var_pointer, name) SER_MAYBE_RETURN(ser_var(ser, (char *)var_pointer, sizeof(*var_pointer), name, __FILE__, __LINE__))
#define SER_VAR(var_pointer) SER_VAR_NAME(var_pointer, #var_pointer)

enum GameVersion
{
  VInitial,
  VMax, // this minus one will be the version used
};

// @Robust probably get rid of this as separate function, just use SER_VAR
SerMaybeFailure ser_V2(SerState *ser, cpVect *var)
{
  SER_VAR(&var->x);
  SER_VAR(&var->y);
  SER_ASSERT(!isnan(var->x));
  SER_ASSERT(!isnan(var->y));
  return ser_ok;
}

// for when you only need 32 bit float precision in a vector2,
// but it's a double
SerMaybeFailure ser_fV2(SerState *ser, cpVect *var)
{
  float x;
  float y;
  if (ser->serializing)
  {
    x = (float)var->x;
    y = (float)var->y;
  }
  SER_VAR(&x);
  SER_VAR(&y);
  SER_ASSERT(!isnan(x));
  SER_ASSERT(!isnan(y));

  var->x = x;
  var->y = y;
  return ser_ok;
}

SerMaybeFailure ser_f(SerState *ser, double *d)
{
  float f;
  if (ser->serializing)
    f = (float)*d;
  SER_VAR(&f);
  SER_ASSERT(!isnan(f));
  *d = f;
  return ser_ok;
}

SerMaybeFailure ser_bodydata(SerState *ser, struct BodyData *data)
{
  SER_MAYBE_RETURN(ser_V2(ser, &data->pos));
  SER_MAYBE_RETURN(ser_V2(ser, &data->vel));
  SER_VAR(&data->rotation);
  SER_VAR(&data->angular_velocity);
  SER_ASSERT(!isnan(data->rotation));
  SER_ASSERT(!isnan(data->angular_velocity));
  return ser_ok;
}

SerMaybeFailure ser_entityid(SerState *ser, EntityID *id)
{
  SER_VAR(&id->generation);
  SER_VAR(&id->index);
  if (id->generation > 0)
    SER_ASSERT(id->index < ser->max_entity_index);
  return ser_ok;
}

SerMaybeFailure ser_inputframe(SerState *ser, InputFrame *i)
{
  SER_VAR(&i->tick);
  SER_MAYBE_RETURN(ser_fV2(ser, &i->movement));
  SER_VAR(&i->rotation);
  SER_VAR(&i->take_over_squad);
  SER_ASSERT(i->take_over_squad >= 0 || i->take_over_squad == -1);
  SER_ASSERT(i->take_over_squad < SquadLast);
  SER_VAR(&i->accept_cur_squad_invite);
  SER_VAR(&i->reject_cur_squad_invite);
  SER_MAYBE_RETURN(ser_entityid(ser, &i->invite_this_player));

  SER_VAR(&i->seat_action);
  SER_MAYBE_RETURN(ser_fV2(ser, &i->hand_pos));

  SER_VAR(&i->dobuild);
  SER_VAR(&i->build_type);
  SER_ASSERT(i->build_type >= 0);
  SER_ASSERT(i->build_type < BoxLast);
  SER_VAR(&i->build_rotation);

  return ser_ok;
}

SerMaybeFailure ser_no_player(SerState *ser)
{
  bool connected = false;
  SER_VAR_NAME(&connected, "&p->connected");

  return ser_ok;
}

SerMaybeFailure ser_player(SerState *ser, Player *p)
{
  SER_VAR(&p->connected);
  if (p->connected)
  {
    SER_VAR(&p->box_unlocks);

    SER_VAR(&p->squad);
    SER_MAYBE_RETURN(ser_entityid(ser, &p->entity));
    SER_MAYBE_RETURN(ser_entityid(ser, &p->last_used_medbay));
    SER_MAYBE_RETURN(ser_inputframe(ser, &p->input));
  }

  return ser_ok;
}

SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e)
{
  SER_VAR(&e->no_save_to_disk); // @Robust this is always false when saving to disk?
  SER_VAR(&e->generation);
  SER_MAYBE_RETURN(ser_f(ser, &e->damage));

  bool has_body = ser->serializing && e->body != NULL;
  SER_VAR(&has_body);

  if (has_body)
  {
    struct BodyData body_data;
    if (ser->serializing)
      populate(e->body, &body_data);
    SER_MAYBE_RETURN(ser_bodydata(ser, &body_data));
    if (!ser->serializing)
    {
      create_body(gs, e);
      update_from(e->body, &body_data);
    }
  }

  bool has_shape = ser->serializing && e->shape != NULL;
  SER_VAR(&has_shape);

  if (has_shape)
  {
    SER_MAYBE_RETURN(ser_fV2(ser, &e->shape_size));
    SER_MAYBE_RETURN(ser_entityid(ser, &e->shape_parent_entity));
    Entity *parent = get_entity(gs, e->shape_parent_entity);
    SER_ASSERT(parent != NULL);

    cpVect shape_pos;
    if (ser->serializing)
      shape_pos = entity_shape_pos(e);
    SER_MAYBE_RETURN(ser_fV2(ser, &shape_pos));

    double shape_mass;
    if (ser->serializing)
      shape_mass = entity_shape_mass(e);
    SER_VAR(&shape_mass);
    SER_ASSERT(!isnan(shape_mass));

    cpShapeFilter filter;
    if (ser->serializing)
    {
      filter = cpShapeGetFilter(e->shape);
    }
    SER_VAR(&filter.categories);
    SER_VAR(&filter.group);
    SER_VAR(&filter.mask);
    if (!ser->serializing)
    {
      create_rectangle_shape(gs, e, parent, shape_pos, e->shape_size, shape_mass);
      cpShapeSetFilter(e->shape, filter);
    }
  }

  if (!ser->save_or_load_from_disk)
  {
    SER_MAYBE_RETURN(ser_f(ser, &e->time_was_last_cloaked));
  }

  SER_VAR(&e->owning_squad);

  SER_VAR(&e->is_player);
  if (e->is_player)
  {
    SER_ASSERT(e->no_save_to_disk);

    SER_MAYBE_RETURN(ser_entityid(ser, &e->currently_inside_of_box));
    SER_VAR(&e->squad_invited_to);
    SER_MAYBE_RETURN(ser_f(ser, &e->goldness));
  }

  SER_VAR(&e->is_explosion);
  if (e->is_explosion)
  {
    SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_pos));
    SER_MAYBE_RETURN(ser_V2(ser, &e->explosion_vel));
    SER_MAYBE_RETURN(ser_f(ser, &e->explosion_progress));
    SER_MAYBE_RETURN(ser_f(ser, &e->explosion_push_strength));
    SER_MAYBE_RETURN(ser_f(ser, &e->explosion_radius));
  }

  SER_VAR(&e->is_sun);
  if (e->is_sun)
  {
    SER_MAYBE_RETURN(ser_V2(ser, &e->sun_vel));
    SER_MAYBE_RETURN(ser_V2(ser, &e->sun_pos));
    SER_MAYBE_RETURN(ser_f(ser, &e->sun_mass));
    SER_MAYBE_RETURN(ser_f(ser, &e->sun_radius));
  }

  SER_VAR(&e->is_grid);
  if (e->is_grid)
  {
    SER_MAYBE_RETURN(ser_f(ser, &e->total_energy_capacity));
    SER_MAYBE_RETURN(ser_entityid(ser, &e->boxes));
  }

  SER_VAR(&e->is_missile)
  if (e->is_missile)
  {
    SER_MAYBE_RETURN(ser_f(ser, &e->time_burned_for));
  }

  SER_VAR(&e->is_box);
  if (e->is_box)
  {
    SER_VAR(&e->box_type);
    SER_VAR(&e->is_platonic);

    SER_VAR(&e->owning_squad);

    SER_VAR(&e->always_visible);
    SER_MAYBE_RETURN(ser_entityid(ser, &e->next_box));
    SER_MAYBE_RETURN(ser_entityid(ser, &e->prev_box));
    SER_VAR(&e->compass_rotation);
    SER_VAR(&e->indestructible);
    switch (e->box_type)
    {
    case BoxMedbay:
    case BoxCockpit:
      if (!ser->save_or_load_from_disk)
        SER_MAYBE_RETURN(ser_entityid(ser, &e->player_who_is_inside_of_me));
      break;
    case BoxThruster:
    case BoxGyroscope:
      SER_MAYBE_RETURN(ser_f(ser, &e->thrust));
      SER_MAYBE_RETURN(ser_f(ser, &e->wanted_thrust));
      break;
    case BoxBattery:
      SER_MAYBE_RETURN(ser_f(ser, &e->energy_used));
      break;
    case BoxSolarPanel:
      SER_MAYBE_RETURN(ser_f(ser, &e->sun_amount));
      break;
    case BoxScanner:
      SER_MAYBE_RETURN(ser_entityid(ser, &e->currently_scanning));
      SER_MAYBE_RETURN(ser_f(ser, &e->currently_scanning_progress));
      SER_VAR(&e->blueprints_learned);
      SER_MAYBE_RETURN(ser_f(ser, &e->scanner_head_rotate));
      SER_MAYBE_RETURN(ser_fV2(ser, &e->platonic_nearest_direction));
      SER_MAYBE_RETURN(ser_f(ser, &e->platonic_detection_strength));
      break;
    case BoxCloaking:
      SER_MAYBE_RETURN(ser_f(ser, &e->cloaking_power));
      break;
    case BoxMissileLauncher:
      SER_MAYBE_RETURN(ser_f(ser, &e->missile_construction_charge));
      break;
    default:
      break;
    }
  }

  return ser_ok;
}

SerMaybeFailure ser_opus_packets(SerState *ser, Queue *mic_or_speaker_data)
{
  bool no_more_packets = false;
  if (ser->serializing)
  {
    size_t queued = queue_num_elements(mic_or_speaker_data);
    for (size_t i = 0; i < queued; i++)
    {
      SER_VAR(&no_more_packets);
      OpusPacket *cur = (OpusPacket *)queue_pop_element(mic_or_speaker_data);
      bool isnull = cur == NULL;
      SER_VAR(&isnull);
      if (!isnull && cur != NULL) // cur != NULL is to suppress VS warning
      {
        SER_VAR(&cur->length);
        SER_DATA((char *)cur->data, cur->length);
      }
    }
    no_more_packets = true;
    SER_VAR(&no_more_packets);
  }
  else
  {
    while (true)
    {
      SER_VAR(&no_more_packets);
      if (no_more_packets)
        break;
      OpusPacket *cur = (OpusPacket *)queue_push_element(mic_or_speaker_data);
      OpusPacket dummy;
      if (cur == NULL)
        cur = &dummy; // throw away this packet
      bool isnull = false;
      SER_VAR(&isnull);
      if (!isnull)
      {
        SER_VAR(&cur->length);
        SER_ASSERT(cur->length < VOIP_PACKET_MAX_SIZE);
        SER_ASSERT(cur->length >= 0);
        SER_DATA((char *)cur->data, cur->length);
      }
    }
  }
  return ser_ok;
}

SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
{
  SER_VAR(&ser->version);
  SER_ASSERT(ser->version >= 0);
  SER_ASSERT(ser->version < VMax);

  if (!ser->save_or_load_from_disk)
    SER_MAYBE_RETURN(ser_opus_packets(ser, s->audio_playback_buffer));

  GameState *gs = s->cur_gs;

  // completely reset and destroy all gamestate data
  if (!ser->serializing)
  {
    // avoid a memset here very expensive. que rico!
    destroy(gs);
    initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities));
    gs->cur_next_entity = 0; // updated on deserialization
  }

  int cur_next_entity = 0;
  if (ser->serializing)
    cur_next_entity = gs->cur_next_entity;
  SER_VAR(&cur_next_entity);
  SER_ASSERT(cur_next_entity <= ser->max_entity_index);

  SER_VAR(&s->your_player);

  SER_VAR(&gs->tick);
  SER_VAR(&gs->subframe_time);

  SER_MAYBE_RETURN(ser_V2(ser, &gs->goldpos));

  if (!ser->save_or_load_from_disk) // don't save player info to disk, this is filled on connection/disconnection
  {
    // @Robust save player data with their ID or something somehow. Like local backup of their account
    for (size_t i = 0; i < MAX_PLAYERS; i++)
    {
      if (get_entity(gs, gs->players[i].entity) != NULL && is_cloaked(gs, get_entity(gs, gs->players[i].entity), ser->for_player))
      {
        SER_MAYBE_RETURN(ser_no_player(ser));
      }
      else
      {
        SER_MAYBE_RETURN(ser_player(ser, &gs->players[i]));
      }
    }
  }

  for (int i = 0; i < MAX_SUNS; i++)
  {
    bool suns_done = get_entity(gs, gs->suns[i]) == NULL;
    SER_VAR(&suns_done);
    if (suns_done)
      break;
    SER_MAYBE_RETURN(ser_entityid(ser, &gs->suns[i]));
  }

  if (ser->serializing)
  {
    bool entities_done = false;
    for (size_t i = 0; i < gs->cur_next_entity; i++)
    {
      Entity *e = &gs->entities[i];
#define DONT_SEND_BECAUSE_CLOAKED(entity) (!ser->save_or_load_from_disk && ser->for_player != NULL && is_cloaked(gs, entity, ser->for_player))
#define SER_ENTITY()       \
  SER_VAR(&entities_done); \
  SER_VAR(&i);             \
  SER_MAYBE_RETURN(ser_entity(ser, gs, e))
      if (e->exists && !(ser->save_or_load_from_disk && e->no_save_to_disk) && !DONT_SEND_BECAUSE_CLOAKED(e))
      {
        if (!e->is_box && !e->is_grid)
        {
          SER_ENTITY();
        }
        if (e->is_grid)
        {
          bool serialized_grid_yet = false;
          // serialize boxes always after bodies, so that by the time the boxes
          // are loaded in the parent body is loaded in and can be referenced.
          BOXES_ITER(gs, cur_box, e)
          {
            bool this_box_in_range = ser->save_or_load_from_disk;
            this_box_in_range |= ser->for_player == NULL;
            this_box_in_range |= (ser->for_player != NULL && cpvdistsq(entity_pos(ser->for_player), entity_pos(cur_box)) < VISION_RADIUS * VISION_RADIUS); // only in vision radius
            if (DONT_SEND_BECAUSE_CLOAKED(cur_box))
              this_box_in_range = false;
            if (cur_box->always_visible)
              this_box_in_range = true;
            if (this_box_in_range)
            {
              if (!serialized_grid_yet)
              {
                serialized_grid_yet = true;
                SER_ENTITY();
              }

              // serialize this box
              EntityID cur_id = get_id(gs, cur_box);
              SER_ASSERT(cur_id.index < gs->max_entities);
              SER_VAR(&entities_done);
              size_t the_index = (size_t)cur_id.index; // super critical. Type of &i is size_t. @Robust add debug info in serialization for what size the expected type is, maybe string nameof the type
              SER_VAR_NAME(&the_index, "&i");
              SER_MAYBE_RETURN(ser_entity(ser, gs, cur_box));
            }
          }
        }
      }
#undef SER_ENTITY
    }
    entities_done = true;
    SER_VAR(&entities_done);
  }
  else
  {
    Entity *last_grid = NULL;
    while (true)
    {
      bool entities_done = false;
      SER_VAR(&entities_done);
      if (entities_done)
        break;
      size_t next_index;
      SER_VAR_NAME(&next_index, "&i");
      SER_ASSERT(next_index < gs->max_entities);
      SER_ASSERT(next_index >= 0);
      Entity *e = &gs->entities[next_index];
      e->exists = true;
      // unsigned int possible_next_index = (unsigned int)(next_index + 2); // plus two because player entity refers to itself on deserialization
      unsigned int possible_next_index = (unsigned int)(next_index + 1);
      gs->cur_next_entity = gs->cur_next_entity < possible_next_index ? possible_next_index : gs->cur_next_entity;
      SER_MAYBE_RETURN(ser_entity(ser, gs, e));

      if (e->is_box)
      {
        SER_ASSERT(last_grid != NULL);
        SER_ASSERT(get_entity(gs, e->shape_parent_entity) != NULL);
        SER_ASSERT(last_grid == get_entity(gs, e->shape_parent_entity));
        e->prev_box = (EntityID){0};
        e->next_box = (EntityID){0};
        box_add_to_boxes(gs, last_grid, e);
      }

      if (e->is_grid)
      {
        e->boxes = (EntityID){0};
        last_grid = e;
      }
    }
    for (size_t i = 0; i < gs->cur_next_entity; i++)
    {
      Entity *e = &gs->entities[i];
      if (!e->exists)
      {
        if (e->generation == 0)
          e->generation = 1; // 0 generation reference is invalid, means null
        e->next_free_entity = gs->free_list;
        gs->free_list = get_id(gs, e);
      }
    }
  }
  return ser_ok;
}

// for_this_player can be null then the entire world will be sent
bool server_to_client_serialize(struct ServerToClient *msg, unsigned char *bytes, size_t *out_len, size_t max_len, Entity *for_this_player, bool to_disk)
{
  assert(msg->cur_gs != NULL);
  assert(msg != NULL);

  SerState ser = (SerState){
      .bytes = bytes,
      .serializing = true,
      .cursor = 0,
      .max_size = max_len,
      .for_player = for_this_player,
      .max_entity_index = msg->cur_gs->cur_next_entity,
      .version = VMax - 1,
  };

  if (for_this_player == NULL) // @Robust jank
  {
    ser.save_or_load_from_disk = true;
  }

  ser.write_varnames = to_disk;
#ifdef WRITE_VARNAMES
  ser.write_varnames = true;
#endif

  SerMaybeFailure result = ser_server_to_client(&ser, msg);
  *out_len = ser.cursor + 1; // @Robust not sure why I need to add one to cursor, ser.cursor should be the length..
  if (result.failed)
  {
    Log("Failed to serialize on line %d because of %s\n", result.line, result.expression);
    return false;
  }
  else
  {
    return true;
  }
}

bool server_to_client_deserialize(struct ServerToClient *msg, unsigned char *bytes, size_t max_len, bool from_disk)
{
  assert(msg->cur_gs != NULL);
  assert(msg != NULL);

  SerState servar = (SerState){
      .bytes = bytes,
      .serializing = false,
      .cursor = 0,
      .max_size = max_len,
      .max_entity_index = msg->cur_gs->max_entities,
      .save_or_load_from_disk = from_disk,
  };

  if (from_disk)
    servar.write_varnames = true;

#ifdef WRITE_VARNAMES
  servar.write_varnames = true;
#endif

  SerState *ser = &servar;
  SerMaybeFailure result = ser_server_to_client(ser, msg);
  if (result.failed)
  {
    Log("Failed to deserialize server to client on line %d because of %s\n", result.line, result.expression);
    return false;
  }
  else
  {
    return true;
  }
}

// only serializes up to the maximum inputs the server holds
SerMaybeFailure ser_client_to_server(SerState *ser, ClientToServer *msg)
{
  SER_VAR(&ser->version);
  SER_MAYBE_RETURN(ser_opus_packets(ser, msg->mic_data));

  // serialize input packets
  size_t num;
  if (ser->serializing)
  {
    num = queue_num_elements(msg->input_data);
    if (num > INPUT_QUEUE_MAX)
      num = INPUT_QUEUE_MAX;
  }
  SER_VAR(&num);
  SER_ASSERT(num <= INPUT_QUEUE_MAX);
  if (ser->serializing)
  {
    size_t to_skip = queue_num_elements(msg->input_data) - num;
    size_t i = 0;
    QUEUE_ITER(msg->input_data, cur_header)
    {
      if (i < to_skip)
      {
        i++;
      }
      else
      {
        InputFrame *cur = (InputFrame *)cur_header->data;
        SER_MAYBE_RETURN(ser_inputframe(ser, cur));
      }
    }
  }
  else
  {
    for (size_t i = 0; i < num; i++)
    {
      InputFrame *new_frame = (InputFrame *)queue_push_element(msg->input_data);
      SER_ASSERT(new_frame != NULL);
      SER_MAYBE_RETURN(ser_inputframe(ser, new_frame));
    }
  }
  return ser_ok;
}

bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, unsigned char *bytes, size_t *out_len, size_t max_len)
{
  SerState ser = (SerState){
      .bytes = bytes,
      .serializing = true,
      .cursor = 0,
      .max_size = max_len,
      .for_player = NULL,
      .max_entity_index = gs->cur_next_entity,
      .version = VMax - 1,
  };
#ifdef WRITE_VARNAMES
  ser.write_varnames = true;
#endif

  SerMaybeFailure result = ser_client_to_server(&ser, msg);
  *out_len = ser.cursor + 1; // see other comment for server to client
  if (result.failed)
  {
    Log("Failed to serialize client to server because %s was false, line %d\n", result.expression, result.line);
    return false;
  }
  else
  {
    return true;
  }
}

bool client_to_server_deserialize(GameState *gs, struct ClientToServer *msg, unsigned char *bytes, size_t max_len)
{
  SerState servar = (SerState){
      .bytes = bytes,
      .serializing = false,
      .cursor = 0,
      .max_size = max_len,
      .max_entity_index = gs->cur_next_entity,
      .save_or_load_from_disk = false,
  };
#ifdef WRITE_VARNAMES
  servar.write_varnames = true;
#endif

  SerState *ser = &servar;
  SerMaybeFailure result = ser_client_to_server(ser, msg);
  if (result.failed)
  {
    Log("Failed to deserialize client to server on line %d because of %s\n", result.line, result.expression);
    return false;
  }
  else
  {
    return true;
  }
}

static THREADLOCAL Entity *grid_to_exclude = NULL;
static bool merge_filter(Entity *potential_merge)
{
  assert(grid_to_exclude != NULL);
  assert(grid_to_exclude->is_grid);
  return potential_merge->is_box && potential_merge->box_type == BoxMerge && box_grid(potential_merge) != grid_to_exclude;
}

static void cloaking_shield_callback_func(cpShape *shape, cpContactPointSet *points, void *data)
{
  Entity *from_cloaking_box = (Entity *)data;
  GameState *gs = entitys_gamestate(from_cloaking_box);
  Entity *to_cloak = cp_shape_entity(shape);

  to_cloak->time_was_last_cloaked = time(gs);
  to_cloak->last_cloaked_by_squad = from_cloaking_box->owning_squad;
}

// has to be global var because can only get this information
static THREADLOCAL cpShape *closest_to_point_in_radius_result = NULL;
static THREADLOCAL double closest_to_point_in_radius_result_largest_dist = 0.0;
static THREADLOCAL bool (*closest_to_point_in_radius_filter_func)(Entity *);
static void closest_point_callback_func(cpShape *shape, cpContactPointSet *points, void *data)
{
  assert(points->count == 1);
  Entity *e = cp_shape_entity(shape);
  if (!e->is_box)
    return;

  if (closest_to_point_in_radius_filter_func != NULL && !closest_to_point_in_radius_filter_func(e))
    return;
  double dist = cpvlength((cpvsub(points->points[0].pointA, points->points[0].pointB)));
  // double dist = -points->points[0].distance;
  if (dist > closest_to_point_in_radius_result_largest_dist)
  {
    closest_to_point_in_radius_result_largest_dist = dist;
    closest_to_point_in_radius_result = shape;
  }
}

// filter func null means everything is ok, if it's not null and returns false, that means
// exclude it from the selection. This returns the closest box entity!
Entity *closest_box_to_point_in_radius(struct GameState *gs, cpVect point, double radius, bool (*filter_func)(Entity *))
{
  closest_to_point_in_radius_result = NULL;
  closest_to_point_in_radius_result_largest_dist = 0.0;
  closest_to_point_in_radius_filter_func = filter_func;
  cpBody *tmpbody = cpBodyNew(0.0, 0.0);
  cpShape *circle = cpCircleShapeNew(tmpbody, radius, (point));
  cpSpaceShapeQuery(gs->space, circle, closest_point_callback_func, NULL);

  cpShapeFree(circle);
  cpBodyFree(tmpbody);

  if (closest_to_point_in_radius_result != NULL)
  {
    // @Robust query here for only boxes that are part of ships, could get nasty...
    return cp_shape_entity(closest_to_point_in_radius_result);
  }

  return NULL;
}

static THREADLOCAL BOX_UNLOCKS_TYPE scanner_has_learned = 0;
static bool scanner_filter(Entity *e)
{
  if (!e->is_box)
    return false;
  if (learned_boxes_has_box(scanner_has_learned, e->box_type))
    return false;
  return true;
}

static double cur_explosion_damage = 0.0;
static cpVect explosion_origin = {0};
static double explosion_push_strength = 0.0;
static void explosion_callback_func(cpShape *shape, cpContactPointSet *points, void *data)
{
  GameState *gs = (GameState *)data;
  cp_shape_entity(shape)->damage += cur_explosion_damage;
  Entity *parent = get_entity(gs, cp_shape_entity(shape)->shape_parent_entity);
  cpVect from_pos = entity_pos(cp_shape_entity(shape));
  cpVect impulse = cpvmult(cpvnormalize(cpvsub(from_pos, explosion_origin)), explosion_push_strength);
  assert(parent->body != NULL);
  cpBodyApplyImpulseAtWorldPoint(parent->body, (impulse), (from_pos));
}

static void do_explosion(GameState *gs, Entity *explosion, double dt)
{

  cpBody *tmpbody = cpBodyNew(0.0, 0.0);
  cpShape *circle = cpCircleShapeNew(tmpbody, explosion->explosion_radius, (explosion_origin));

  cur_explosion_damage = dt * EXPLOSION_DAMAGE_PER_SEC;
  explosion_origin = explosion->explosion_pos;
  explosion_push_strength = explosion->explosion_push_strength;
  cpSpaceShapeQuery(gs->space, circle, explosion_callback_func, (void *)gs);

  cpShapeFree(circle);
  cpBodyFree(tmpbody);
}

cpVect box_facing_vector(Entity *box)
{
  assert(box->is_box);
  cpVect to_return = (cpVect){.x = 1.0, .y = 0.0};

  to_return = box_compass_vector(box);
  to_return = cpvspin(to_return, box_rotation(box));

  return to_return;
}

enum CompassRotation facing_vector_to_compass(Entity *grid_to_transplant_to, Entity *grid_facing_vector_from, cpVect facing_vector)
{
  assert(grid_to_transplant_to->body != NULL);
  assert(grid_to_transplant_to->is_grid);

  cpVect local_to_from = grid_world_to_local(grid_facing_vector_from, cpvadd(entity_pos(grid_facing_vector_from), facing_vector));
  Log("local %f %f\n", local_to_from.x, local_to_from.y);

  cpVect from_target = cpvadd(entity_pos(grid_to_transplant_to), facing_vector);
  cpVect local_target = grid_world_to_local(grid_to_transplant_to, from_target);
  cpVect local_facing = local_target;

  enum CompassRotation dirs[] = {
      Right,
      Left,
      Up,
      Down};

  int smallest = -1;
  double smallest_dist = INFINITY;
  for (int i = 0; i < ARRLEN(dirs); i++)
  {
    cpVect point = cpvspin((cpVect){.x = 1.0}, rotangle(dirs[i]));
    double dist = cpvdist(point, local_facing);
    if (dist < smallest_dist)
    {
      smallest_dist = dist;
      smallest = i;
    }
  }
  assert(smallest != -1);
  return dirs[smallest];
}

cpVect thruster_force(Entity *box)
{
  return cpvmult(box_facing_vector(box), -box->thrust * THRUSTER_FORCE);
}

uint64_t tick(GameState *gs)
{
  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)
{
  return box_grid(closest_box_to_point_in_radius(gs, world_hand_pos, BUILD_BOX_SNAP_DIST_TO_SHIP, NULL));
}

cpVect potentially_snap_hand_pos(GameState *gs, cpVect world_hand_pos)
{
  Entity *potential_grid = grid_to_build_on(gs, world_hand_pos);
  if (potential_grid != NULL)
  {
    world_hand_pos = grid_snapped_box_pos(potential_grid, world_hand_pos);
  }
  return world_hand_pos;
}

cpVect get_world_hand_pos(GameState *gs, InputFrame *input, Entity *player)
{
  return potentially_snap_hand_pos(gs, cpvadd(entity_pos(player), input->hand_pos));
}

bool batteries_have_capacity_for(GameState *gs, Entity *grid, double *energy_left_over, double energy_to_use)
{
  double seen_energy = 0.0;
  BOXES_ITER(gs, possible_battery, grid)
  {
    if (possible_battery->box_type == BoxBattery)
    {
      Entity *battery = possible_battery;
      seen_energy += BATTERY_CAPACITY - battery->energy_used;
      if (seen_energy >= energy_to_use + *energy_left_over)
        return true;
    }
  }
  return false;
}

// returns any energy unable to burn
double batteries_use_energy(GameState *gs, Entity *grid, double *energy_left_over, double energy_to_use)
{
  if (*energy_left_over > 0.0)
  {
    double energy_to_use_from_leftover = fmin(*energy_left_over, energy_to_use);
    *energy_left_over -= energy_to_use_from_leftover;
    energy_to_use -= energy_to_use_from_leftover;
  }
  BOXES_ITER(gs, possible_battery, grid)
  {
    if (possible_battery->box_type == BoxBattery)
    {
      Entity *battery = possible_battery;
      double energy_to_burn_from_this_battery = fmin(BATTERY_CAPACITY - battery->energy_used, energy_to_use);
      battery->energy_used += energy_to_burn_from_this_battery;
      energy_to_use -= energy_to_burn_from_this_battery;
      if (energy_to_use <= 0.0)
        return 0.0;
    }
  }
  return energy_to_use;
}

double sun_dist_no_gravity(Entity *sun)
{
  // return (GRAVITY_CONSTANT * (SUN_MASS * mass / (distance * distance))) / mass;
  // 0.01 = (GRAVITY_CONSTANT * (SUN_MASS / (distance_sqr)));
  // 0.01 / GRAVITY_CONSTANT = SUN_MASS / distance_sqr;
  // distance = sqrt( SUN_MASS / (0.01 / GRAVITY_CONSTANT) )
  return sqrt(sun->sun_mass / (GRAVITY_SMALLEST / GRAVITY_CONSTANT));
}

double entity_mass(Entity *m)
{
  if (m->body != NULL)
    return (float)cpBodyGetMass(m->body);
  else if (m->is_box)
    return BOX_MASS;
  else if (m->is_sun)
    return m->sun_mass;
  else
  {
    assert(false);
    return 0.0;
  }
}

cpVect sun_gravity_accel_for_entity(Entity *entity_with_gravity, Entity *sun)
{
#ifdef NO_GRAVITY
  return (cpVect){0};
#else

  if (cpvlength(cpvsub(entity_pos(entity_with_gravity), entity_pos(sun))) > sun_dist_no_gravity(sun))
    return (cpVect){0};
  cpVect rel_vector = cpvsub(entity_pos(entity_with_gravity), entity_pos(sun));
  double mass = entity_mass(entity_with_gravity);
  assert(mass != 0.0);
  double distance_sqr = cpvlengthsq(rel_vector);
  // return (GRAVITY_CONSTANT * (SUN_MASS * mass / (distance * distance))) / mass;
  // the mass divides out

  // on top
  double accel_magnitude = (GRAVITY_CONSTANT * (sun->sun_mass / (distance_sqr)));
  if (distance_sqr <= sun->sun_radius)
  {
    accel_magnitude *= -1.0;
    if (distance_sqr <= sun->sun_radius * 0.25)
      accel_magnitude = 0.0;
  }
  cpVect towards_sun = cpvnormalize(cpvmult(rel_vector, -1.0));
  return cpvmult(towards_sun, accel_magnitude);
#endif // NO_GRAVITY
}

void entity_set_velocity(Entity *e, cpVect vel)
{
  if (e->body != NULL)
    cpBodySetVelocity(e->body, (vel));
  else if (e->is_sun)
    e->sun_vel = vel;
  else
    assert(false);
}

void entity_ensure_in_orbit(GameState *gs, Entity *e)
{
  cpVect total_new_vel = {0};
  SUNS_ITER(gs)
  {
    cpVect gravity_accel = sun_gravity_accel_for_entity(e, i.sun);
    if (cpvlength(gravity_accel) > 0.0)
    {
      double dist = cpvlength(cpvsub(entity_pos(e), entity_pos(i.sun)));
      cpVect orthogonal_to_gravity = cpvnormalize(cpvspin(gravity_accel, PI / 2.0));
      cpVect wanted_vel = cpvmult(orthogonal_to_gravity, sqrt(cpvlength(gravity_accel) * dist));

      total_new_vel = cpvadd(total_new_vel, (wanted_vel));
    }
  }
  entity_set_velocity(e, (total_new_vel));

  // cpVect pos = (cpvsub(entity_pos(e), SUN_POS));
  // cpFloat r = cpvlength(pos);
  // cpFloat v = cpfsqrt(sun_gravity_accel_at_point((pos), e) / r) / r;
  // cpBodySetVelocity(e->body, cpvmult(cpvperp(pos), v));
}

cpVect box_vel(Entity *box)
{
  assert(box->is_box);
  Entity *grid = box_grid(box);
  return (cpBodyGetVelocityAtWorldPoint(grid->body, (entity_pos(box))));
}

void create_bomb_station(GameState *gs, cpVect pos, enum BoxType platonic_type)
{

  enum CompassRotation rot = Right;

#define BOX_AT_TYPE(grid, pos, type)      \
  {                                       \
    Entity *box = new_entity(gs);         \
    box_create(gs, box, grid, pos);       \
    box->box_type = type;                 \
    box->compass_rotation = rot;          \
    box->indestructible = indestructible; \
  }
#define BOX_AT(grid, pos) BOX_AT_TYPE(grid, pos, BoxHullpiece)

  bool indestructible = false;
  Entity *grid = new_entity(gs);
  grid_create(gs, grid);
  entity_set_pos(grid, pos);
  Entity *platonic_box = new_entity(gs);
  box_create(gs, platonic_box, grid, (cpVect){0});
  platonic_box->box_type = platonic_type;
  platonic_box->is_platonic = true;
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE, 0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 2, 0}), BoxHullpiece);
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 3, 0}), BoxHullpiece);
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 4, 0}), BoxHullpiece);

  indestructible = true;
  for (double y = -BOX_SIZE * 5.0; y <= BOX_SIZE * 5.0; y += BOX_SIZE)
  {
    BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 5.0, y}), BoxHullpiece);
  }
  for (double x = -BOX_SIZE * 5.0; x <= BOX_SIZE * 5.0; x += BOX_SIZE)
  {
    BOX_AT_TYPE(grid, ((cpVect){x, BOX_SIZE * 5.0}), BoxHullpiece);
    BOX_AT_TYPE(grid, ((cpVect){x, -BOX_SIZE * 5.0}), BoxHullpiece);
  }
  indestructible = false;
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, BOX_SIZE * 5.0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, BOX_SIZE * 3.0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, BOX_SIZE * 1.0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, -BOX_SIZE * 2.0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, -BOX_SIZE * 3.0}), BoxExplosive);
  BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 6.0, -BOX_SIZE * 5.0}), BoxExplosive);

  entity_ensure_in_orbit(gs, grid);
}

void create_hard_shell_station(GameState *gs, cpVect pos, enum BoxType platonic_type)
{

  enum CompassRotation rot = Right;

  bool indestructible = false;
  Entity *grid = new_entity(gs);
  grid_create(gs, grid);
  entity_set_pos(grid, pos);
  Entity *platonic_box = new_entity(gs);
  box_create(gs, platonic_box, grid, (cpVect){0});
  platonic_box->box_type = platonic_type;
  platonic_box->is_platonic = true;
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 2, 0}), BoxHullpiece);
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 3, 0}), BoxHullpiece);
  BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 4, 0}), BoxHullpiece);

  indestructible = true;
  for (double y = -BOX_SIZE * 5.0; y <= BOX_SIZE * 5.0; y += BOX_SIZE)
  {
    BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE * 5.0, y}), BoxHullpiece);
    BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE * 5.0, y}), BoxHullpiece);
  }
  for (double x = -BOX_SIZE * 5.0; x <= BOX_SIZE * 5.0; x += BOX_SIZE)
  {
    BOX_AT_TYPE(grid, ((cpVect){x, BOX_SIZE * 5.0}), BoxHullpiece);
    BOX_AT_TYPE(grid, ((cpVect){x, -BOX_SIZE * 5.0}), BoxHullpiece);
  }
  entity_ensure_in_orbit(gs, grid);
  indestructible = false;
}
void create_initial_world(GameState *gs)
{
  EntityID suns[] = {
      create_sun(gs, new_entity(gs), ((cpVect){100.0, 0.0}), ((cpVect){0.0, 0.0}), 100000.0, 20.0),
      create_sun(gs, new_entity(gs), ((cpVect){100.0, 50.0}), ((cpVect){10.0, 0.0}), 10000.0, 10.0),
      create_sun(gs, new_entity(gs), ((cpVect){100.0, -50.0}), ((cpVect){-10.0, 0.0}), 10000.0, 10.0),
      create_sun(gs, new_entity(gs), ((cpVect){50.0, 200.0}), ((cpVect){5.0, 0.0}), 400000.0, 30.0),
      create_sun(gs, new_entity(gs), ((cpVect){-200.0, 200.0}), ((cpVect){-15.0, 0.0}), 900000.0, 60.0),
  };

  for (int i = 0; i < ARRLEN(suns); i++)
  {
    gs->suns[i] = suns[i];
  }

#ifdef DEBUG_WORLD
  Log("Creating debug world\n");
  // pos, mass, radius
  create_bomb_station(gs, (cpVect){-5.0, 0.0}, BoxExplosive);
  create_bomb_station(gs, (cpVect){0.0, 5.0}, BoxGyroscope);
  create_hard_shell_station(gs, (cpVect){-5.0, 5.0}, BoxCloaking);

  bool indestructible = false;

  double theta = deg2rad(65.0);

  cpVect from = (cpVect){BOX_SIZE * 4.0, -1};

  enum CompassRotation rot = Right;
  {

    Entity *grid = new_entity(gs);
    grid_create(gs, grid);
    entity_set_pos(grid, cpvadd(from, cpvspin((cpVect){.x = -BOX_SIZE * 9.0}, theta)));
    cpBodySetAngle(grid->body, theta + PI);
    rot = Left;
    BOX_AT_TYPE(grid, ((cpVect){0.0, 0.0}), BoxMerge);
    BOX_AT(grid, ((cpVect){0.0, -BOX_SIZE}));
    BOX_AT_TYPE(grid, ((cpVect){BOX_SIZE, 0.0}), BoxMerge);
    entity_ensure_in_orbit(gs, grid);
  }

  {
    Entity *grid = new_entity(gs);
    grid_create(gs, grid);
    entity_set_pos(grid, from);
    cpBodySetAngle(grid->body, theta);
    rot = Left;
    BOX_AT_TYPE(grid, ((cpVect){-BOX_SIZE, 0.0}), BoxMerge);
    rot = Down;
    BOX_AT_TYPE(grid, ((cpVect){0.0, 0.0}), BoxMerge);
    rot = Up;
    BOX_AT_TYPE(grid, ((cpVect){0.0, BOX_SIZE}), BoxMerge);
    cpBodySetVelocity(grid->body, (cpvspin((cpVect){-0.4, 0.0}, theta)));
    entity_ensure_in_orbit(gs, grid);
  }

#else
  create_bomb_station(gs, (cpVect){-200.0, 0.0}, BoxExplosive);
  create_hard_shell_station(gs, (cpVect){0.0, 400.0}, BoxGyroscope);
  create_bomb_station(gs, (cpVect){0.0, -150.0}, BoxCloaking);
  create_bomb_station(gs, (cpVect){300.0, 300.0}, BoxMissileLauncher);
  create_hard_shell_station(gs, (cpVect){50.0, 100.0}, BoxMerge);
#endif
}

void exit_seat(GameState *gs, Entity *seat_in, Entity *p)
{
  cpVect pilot_seat_exit_spot = cpvadd(entity_pos(seat_in), cpvmult(box_facing_vector(seat_in), BOX_SIZE));
  cpBodySetPosition(p->body, (pilot_seat_exit_spot));
  // cpBodySetVelocity(p->body, (player_vel(gs, p)));
  cpBodySetVelocity(p->body, cpBodyGetVelocity(box_grid(seat_in)->body));
}

void process(struct GameState *gs, double dt, bool is_subframe)
{
  PROFILE_SCOPE("Gameplay processing")
  {
    assert(gs->space != NULL);

    PROFILE_SCOPE("subframe stuff")
    {

      if (is_subframe)
      {
        gs->subframe_time += dt;
        while (gs->subframe_time > TIMESTEP)
        {
          gs->subframe_time -= TIMESTEP;
          gs->tick++;
        }
      }
      else
      {
        assert(gs->subframe_time == 0.0);
        gs->tick++;
      }
    }

    PROFILE_SCOPE("sun gravity")
    {
      SUNS_ITER(gs)
      {
        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));

        if (cpvlength(from_sun->sun_pos) >= INSTANT_DEATH_DISTANCE_FROM_CENTER)
        {
          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
      }
    }

    PROFILE_SCOPE("input processing")
    {

      PLAYERS_ITER(gs->players, player)
      {
        if (player->input.take_over_squad >= 0)
        {
          if (player->input.take_over_squad == SquadNone)
          {
            player->squad = SquadNone;
          }
          else
          {
            bool squad_taken = false;
            PLAYERS_ITER(gs->players, other_player)
            {
              if (other_player->squad == player->input.take_over_squad)
              {
                squad_taken = true;
                break;
              }
            }
            if (!squad_taken)
              player->squad = player->input.take_over_squad;
          }
          player->input.take_over_squad = -1;
        }

        // squad invites
        Entity *possibly_to_invite = get_entity(gs, player->input.invite_this_player);
        if (player->input.invite_this_player.generation > 0)
          player->input.invite_this_player = (EntityID){0}; // just in case
        if (player->squad != SquadNone && possibly_to_invite != NULL && possibly_to_invite->is_player)
        {
          possibly_to_invite->squad_invited_to = player->squad;
        }
        Entity *p = get_entity(gs, player->entity);
        // 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)
          {
            p->squad_invited_to = SquadNone;
            player->input.reject_cur_squad_invite = false;
          }
        }

#ifdef INFINITE_RESOURCES
        p->damage = 0.0;
#endif
        // update gold win condition
        if (cpvlength(cpvsub((cpBodyGetPosition(p->body)), gs->goldpos)) < GOLD_COLLECT_RADIUS)
        {
          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};
        }
#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
          {
            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);

              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
          {
            exit_seat(gs, seat_maybe_in, p);
            seat_maybe_in->player_who_is_inside_of_me = (EntityID){0};
            p->currently_inside_of_box = (EntityID){0};
          }
        }
#endif

        // process movement
        {
          // no cheating by making movement bigger than length 1
          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)
          {
            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)
            {
              Entity *g = get_entity(gs, seat_inside_of->shape_parent_entity);

              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)
                {

                  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 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)
            {
              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);
            }
          }
          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
            {
              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;
            }
          }
        }
#endif
        if (p->damage >= 1.0)
        {
          entity_destroy(gs, p);
          player->entity = (EntityID){0};
        }

        p->damage = clamp01(p->damage);
      }
    }

    PROFILE_SCOPE("process entities")
    {
      for (size_t i = 0; i < gs->cur_next_entity; i++)
      {
        Entity *e = &gs->entities[i];
        if (!e->exists)
          continue;

        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))
          {
            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;
          }
        }

        // sun processing for this current entity
        PROFILE_SCOPE("this entity sun processing")
        {
          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!)
            {
              PROFILE_SCOPE("Grid processing")
              {
                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)
            {
              PROFILE_SCOPE("Body processing")
              {
                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 (e->is_explosion)
        {
          PROFILE_SCOPE("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);
            }
          }
        }

        if (e->is_missile)
        {
          PROFILE_SCOPE("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 (e->is_box)
        {
          PROFILE_SCOPE("Box processing")
          {
            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);

              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 (e->is_grid)
        {
          PROFILE_SCOPE("Grid processing")
          {
            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)
                {
                  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;
              }
            }

            // 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)
              {
                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)
              {
                Entity *potential_meatbag_to_heal = get_entity(gs, cur_box->player_who_is_inside_of_me);
                if (potential_meatbag_to_heal != NULL)
                {
                  double wanted_energy_use = fmin(potential_meatbag_to_heal->damage, PLAYER_ENERGY_RECHARGE_PER_SECOND * dt);
                  if (wanted_energy_use > 0.0)
                  {
                    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 (cur_box->box_type == BoxCloaking)
              {
                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);
                }
              }
              if (cur_box->box_type == BoxMissileLauncher)
              {
                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);

                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;
                }

                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);
                }

                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);
              }
            }
          }
        }
      }
    }

    PROFILE_SCOPE("chipmunk physics processing")
    {
      cpSpaceStep(gs->space, dt);
    }
  }
}
