#pragma once #include "buildsettings.h" #define MAX_BOX_TYPES 64 #define ZOOM_MIN 0.04 // smaller means you can zoom out more #define ZOOM_MAX 1500.0 // bigger means you can zoom in more #define MAX_PLAYERS 16 #define MAX_SUNS 8 #define MAX_ENTITIES 1024 * 25 #define BOX_SIZE 0.25f // whole size, not half size #define MERGE_MAX_DIST (BOX_SIZE / 2.0f + 0.01f) #define MISSILE_RANGE 4.0f #define MISSILE_BURN_TIME 1.5f #define MISSILE_ARM_TIME 0.5f #define MISSILE_BURN_FORCE 4.0f #define MISSILE_MASS 1.0f // how many missiles grown per second #define MISSILE_DAMAGE_THRESHOLD 0.2f #define MISSILE_CHARGE_RATE 0.5f // centered on the sprite #define MISSILE_SPRITE_SIZE ((cpVect){.x = BOX_SIZE, .y = BOX_SIZE}) #define MISSILE_COLLIDER_SIZE ((cpVect){.x = BOX_SIZE * 0.5f, .y = BOX_SIZE * 0.5f}) #define MISSILE_SPAWN_DIST (sqrt((BOX_SIZE / 2.0) * (BOX_SIZE / 2.0) * 2.0) + MISSILE_COLLIDER_SIZE.x / 2.0 + 0.1) #define MISSILE_EXPLOSION_PUSH 2.5f #define MISSILE_EXPLOSION_RADIUS 0.4f #define PLAYER_SIZE ((cpVect){.x = BOX_SIZE, .y = BOX_SIZE}) #define PLAYER_MASS 0.5f #ifdef FAT_THRUSTERS #define PLAYER_JETPACK_FORCE 200.0f #else #define PLAYER_JETPACK_FORCE 3.5f #endif #define PLAYER_JETPACK_TORQUE 0.05f #define PLAYER_JETPACK_ROTATION_ENERGY_PER_SECOND 0.2f #define PLAYER_JETPACK_SPICE_PER_SECOND 0.08f #define PLAYER_BIG_SCALING 300.0 #define ORB_MASS 4.0 #define ORB_RADIUS 1.0 #define ORB_HEAT_FORCE_MULTIPLIER 5.0 #define ORB_DRAG_CONSTANT 1.0 #define ORB_FROZEN_DRAG_CONSTANT 10.0 #define ORB_HEAT_MAX_DETECTION_DIST 100.0 #define ORB_HEAL_RATE 0.2 #define ORB_MAX_FORCE 200.0 #define VISION_RADIUS 20.0f #define MAX_HAND_REACH 1.0f #define GOLD_COLLECT_RADIUS 0.3f #define BUILD_BOX_SNAP_DIST_TO_SHIP 0.2 #define BOX_MASS 1.0 #define COLLISION_DAMAGE_SCALING 0.15 #define THRUSTER_FORCE 24.0 #define THRUSTER_ENERGY_USED_PER_SECOND 0.005 #define THRUSTER_DAMAGE_PER_SEC 2.0 #define LANDING_GEAR_MAX_DIST (BOX_SIZE * 0.05) #define GYROSCOPE_ENERGY_USED_PER_SECOND 0.005f #define GYROSCOPE_TORQUE 1.5f #define GYROSCOPE_PROPORTIONAL_INERTIAL_RESPONSE 0.7 // between 0-1. How strongly responds to rotation, to stop the rotation #define CLOAKING_ENERGY_USE 0.1f #define CLOAKING_PANEL_SIZE BOX_SIZE * 3.0f #define SCANNER_ENERGY_USE 0.05f #define SCANNER_SCAN_RATE 0.5f #define SCANNER_RADIUS 1.0f #define SCANNER_MAX_RANGE 2000.0 #define SCANNER_MAX_VIEWPORT_RANGE 400.0 #define SCANNER_MIN_RANGE 1.0 #define SCANNER_MAX_POINTS 10 #define SCANNER_MAX_PLATONICS 3 #define MAX_SERVER_TO_CLIENT 1024 * 512 // maximum size of serialized gamestate buffer #define MAX_CLIENT_TO_SERVER 1024 * 10 // maximum size of serialized inputs and mic data #define GRAVITY_CONSTANT 0.01f #define GRAVITY_SMALLEST 0.05f // used to determine when gravity is clamped to 0.0f #define INSTANT_DEATH_DISTANCE_FROM_CENTER 10000.0f #define SOLAR_ENERGY_PER_SECOND 0.09f #define DAMAGE_TO_PLAYER_PER_BLOCK 0.1f #define BATTERY_CAPACITY 4.5f #define PLAYER_ENERGY_RECHARGE_PER_SECOND 0.2 #define EXPLOSION_TIME 0.5f #define EXPLOSION_DAMAGE_PER_SEC 10.0f #define EXPLOSION_DAMAGE_THRESHOLD 0.2f // how much damage until it explodes #define GOLD_UNLOCK_RADIUS 1.0f #ifndef TIME_BETWEEN_WORLD_SAVE #define TIME_BETWEEN_WORLD_SAVE 30.0f #endif #define BOMB_EXPLOSION_PUSH 5.0f #define BOMB_EXPLOSION_RADIUS 1.0f // VOIP #define VOIP_PACKET_BUFFER_SIZE 15 // audio. Must be bigger than 2 #define VOIP_EXPECTED_FRAME_COUNT 240 #define VOIP_SAMPLE_RATE (48000 / 2) // in seconds #define VOIP_TIME_PER_PACKET (1.0f / ((float)((float)VOIP_SAMPLE_RATE / VOIP_EXPECTED_FRAME_COUNT))) #define VOIP_PACKET_MAX_SIZE 4000 #define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS * 0.8f) // multiplayer #define CAUTIOUS_MULTIPLIER 0.8 // how overboard to go with the time ahead predicting, makes it less likely that inputs are lost #define TICKS_BEHIND_DO_SNAP 6 // when this many ticks behind, instead of dilating time SNAP to the healthy ticks ahead #define MAX_MS_SPENT_REPREDICTING 30.0f #define TIME_BETWEEN_SEND_GAMESTATE (1.0f / 20.0f) #define TIME_BETWEEN_INPUT_PACKETS (1.0f / 20.0f) #define TIMESTEP (1.0f / 60.0f) // server required to simulate at this, defines what tick the game is on #define LOCAL_INPUT_QUEUE_MAX 90 // please god let you not have more than 90 frames of game latency #define INPUT_QUEUE_MAX 15 // fucks up serialization if you change this, fix it if you do that! #define BOX_UNLOCKS_TYPE uint64_t // cross platform threadlocal variables #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) #define THREADLOCAL __declspec(thread) #else #define THREADLOCAL __thread #endif #define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) #include "cpVect.h" // offers vector functions and types for the structs #include "miniaudio.h" // @Robust BAD. using miniaudio mutex construct for server thread synchronization. AWFUL! #define _USE_MATH_DEFINES #include // sqrt and cos vector functions and PI #include // tick is unsigned integer #include // logging on errors for functions // defined in gamestate.c. Janky #define flight_assert(condition) __flight_assert(condition, __FILE__, __LINE__, #condition) // including headers from headers bad #include // unfortunate but needs cpSpace, cpBody, cpShape etc #include "queue.h" #include #ifndef OPUS_TYPES_H typedef int opus_int32; #endif #ifndef _STDBOOL #define bool _Bool #define false 0 #define true 1 #endif extern FILE *log_file; #include // the time in logging! void fill_time_string(char *to_fill, size_t max_length); #define Log(...) \ { \ char time_string[2048] = {0}; \ fill_time_string(time_string, 2048); \ fprintf(stdout, "%s | %s:%d | ", time_string, __FILE__, __LINE__); \ fprintf(stdout, __VA_ARGS__); \ if (log_file != NULL) \ { \ fprintf(log_file, "%s | %s:%d | ", time_string, __FILE__, __LINE__); \ fprintf(log_file, __VA_ARGS__); \ } \ } enum BoxType { BoxInvalid, // zero initialized box is invalid! BoxHullpiece, BoxThruster, BoxBattery, BoxCockpit, BoxMedbay, BoxSolarPanel, BoxExplosive, BoxScanner, BoxGyroscope, BoxCloaking, BoxMissileLauncher, BoxMerge, BoxLandingGear, BoxLast, }; enum CompassRotation { Right, Down, Left, Up, RotationLast, }; enum Squad { SquadNone, SquadRed, SquadGreen, SquadBlue, SquadPurple, SquadLast, }; // when generation is 0, invalid ID typedef struct EntityID { unsigned int generation; // VERY IMPORTANT if 0 then EntityID points to nothing, generation >= 1 unsigned int index; // index into the entity arena } EntityID; static inline bool entityids_same(EntityID a, EntityID b) { return (a.generation == b.generation) && (a.index == b.index); } enum ScannerPointKind { Platonic, Neutral, Enemy, }; // when updated, must update serialization, comparison in main.c, and the server // on input received processing function typedef struct InputFrame { uint64_t tick; bool been_processed; // not serialized, used by server just to keep track of what inputs have been processed cpVect movement; double rotation; int take_over_squad; // -1 means not taking over any squad bool accept_cur_squad_invite; bool reject_cur_squad_invite; EntityID invite_this_player; // null means inviting nobody! @Robust make it so just sends interact pos input, and server processes who to invite. This depends on client side prediction + proper input processing at the right tick. bool interact_action; bool seat_action; cpVect hand_pos; // local to player transationally but not rotationally bool dobuild; enum BoxType build_type; enum CompassRotation build_rotation; } InputFrame; // the length of this is assumed to be constant typedef struct PlatonicDetection { // only these two are serialized cpVect direction; double intensity; enum BoxType of_type; bool used_in_scanner_closest_lightning_bolts; } PlatonicDetection; typedef struct Entity { bool exists; bool flag_for_destruction; EntityID next_free_entity; unsigned int generation; bool always_visible; // always serialized to the player. bool no_save_to_disk; // stuff generated later on, like player's bodies or space stations that respawn. double damage; // used by box, player, and orb cpBody *body; // used by grid, player, and box cpShape *shape; // see notes on serializing the shape // players and boxes can be cloaked // If this is within 2 timesteps of the current game time, the entity is invisible. double time_was_last_cloaked; enum Squad last_cloaked_by_squad; // for serializing the shape // @Robust remove shape_parent_entity from this struct, use the shape's body to figure out // what the shape's parent entity is bool is_circle_shape; EntityID shape_parent_entity; // can't be zero if shape is nonzero double shape_radius; // only when circle shape cpVect shape_size; // only when rect shape // player bool is_player; enum Squad owning_squad; // also controls what the player can see, because of cloaking! EntityID currently_inside_of_box; enum Squad squad_invited_to; // if squad none, then no squad invite // explosion bool is_explosion; cpVect explosion_pos; cpVect explosion_vel; double explosion_progress; // in seconds double explosion_push_strength; double explosion_radius; // sun bool is_sun; cpVect sun_vel; cpVect sun_pos; double sun_mass; double sun_radius; bool sun_is_safe; // missile bool is_missile; double time_burned_for; // until MISSILE_BURN_TIME. Before MISSILE_ARM_TIME cannot explode // orb bool is_orb; // grids bool is_grid; double total_energy_capacity; EntityID boxes; // boxes bool is_box; enum BoxType box_type; bool is_platonic; // can't be destroyed, unaffected by physical forces EntityID next_box; // for the grid! EntityID prev_box; // doubly linked so can remove in middle of chain enum CompassRotation compass_rotation; bool indestructible; // used by multiple boxes that use power to see if it should show low power warning. // not serialized, populated during client side prediction. For the medbay // is only valid if the medbay has somebody inside of it double energy_effectiveness; // from 0 to 1, how effectively the box is operating given available power // merger bool wants_disconnect; // don't serialized, termporary value not used across frames // missile launcher double missile_construction_charge; // used by medbay and cockpit EntityID player_who_is_inside_of_me; // only serialized when box_type is thruster or gyroscope, used for both. Thrust // can mean rotation thrust! double wanted_thrust; // the thrust command applied to the thruster double thrust; // the actual thrust it can provide based on energy sources in the grid // only gyroscope, velocity not serialized. Cosmetic double gyrospin_angle; double gyrospin_velocity; // only serialized when box_type is battery double energy_used; // battery, between 0 battery capacity. You have to look through code to figure out what that is! haha sucker! // only serialized when box_type is solar panel double sun_amount; // solar panel, between 0 and 1 // cloaking only double cloaking_power; // 0.0 if unable to be used because no power, 1.0 if fully cloaking! // scanner only stuff! EntityID currently_scanning; double currently_scanning_progress; // when 1.0, scans it! BOX_UNLOCKS_TYPE blueprints_learned; double scanner_head_rotate_speed; // not serialized, cosmetic double scanner_head_rotate; // landing gear only cpConstraint *landed_constraint; // when not null, landing gear landed on something. Only valid while shape_to_land_on is a valid reference // to land, set this to the shape to land on. A constraint will be created if it is valid. If it's not, it will be zerod EntityID shape_to_land_on; // checked for surface distance to make sure is valid PlatonicDetection detected_platonics[SCANNER_MAX_PLATONICS]; // intensity of 0.0 means undetected struct ScannerPoint { char kind; // is of ScannerPointKind char x; char y; } scanner_points[SCANNER_MAX_POINTS]; } Entity; typedef struct Player { bool connected; BOX_UNLOCKS_TYPE box_unlocks; // each bit is that box's unlock enum Squad squad; EntityID entity; EntityID last_used_medbay; InputFrame input; } Player; // use i.sun to access the current sun's pointer typedef struct SunIter { int i; Entity *sun; } SunIter; #define SUNS_ITER(gs_ptr) \ for (SunIter i = {0}; i.i < MAX_SUNS; i.i++) \ if ((i.sun = get_entity(gs_ptr, (gs_ptr)->suns[i.i])) != NULL) #define ENTITIES_ITER(gs, cur) \ for (Entity *cur = (gs)->entities; cur < (gs)->entities + (gs)->cur_next_entity; cur++) \ if (cur->exists) // gotta update the serialization functions when this changes typedef struct GameState { cpSpace *space; uint64_t tick; double subframe_time; // @Robust remove this, I don't think it's used anymore Player players[MAX_PLAYERS]; EntityID suns[MAX_SUNS]; // can't have holes in it for serialization cpVect platonic_positions[MAX_BOX_TYPES]; // don't want to search over every entity to get the nearest platonic box! bool server_side_computing; // some things only the server should know and calculate, like platonic locations // Entity arena // entity pointers can't move around because of how the physics engine handles user data. // if you really need this, potentially refactor to store entity IDs instead of pointers // in the shapes and bodies of chipmunk. Would require editing the library I think Entity *entities; unsigned int max_entities; // maximum number of entities possible in the entities list unsigned int cur_next_entity; // next entity to pass on request of a new entity if the free list is empty EntityID free_list; } GameState; #define PLAYERS_ITER(players, cur) \ for (Player *cur = players; cur < players + MAX_PLAYERS; cur++) \ if (cur->connected) #define PI M_PI #define TAU (M_PI * 2.0) // returns in radians static inline double rotangle(enum CompassRotation rot) { switch (rot) { case Right: return 0.0f; break; case Down: return -PI / 2.0f; break; case Left: return -PI; break; case Up: return -3.0f * PI / 2.0f; break; default: Log("Unknown rotation %d\n", rot); return -0.0f; break; } } typedef struct OpusPacket { opus_int32 length; unsigned char data[VOIP_PACKET_MAX_SIZE]; } OpusPacket; typedef struct ServerToClient { struct GameState *cur_gs; Queue *audio_playback_buffer; int your_player; } ServerToClient; typedef struct ClientToServer { Queue *mic_data; // on serialize, flushes this of packets. On deserialize, fills it Queue *input_data; // does not flush on serialize! must be in order of tick } ClientToServer; #define DeferLoop(start, end) \ for (int _i_ = ((start), 0); _i_ == 0; _i_ += 1, (end)) // server void server(void *info); // data parameter required from thread api... void create_player(Player *player); bool box_unlocked(Player *player, enum BoxType box); // gamestate void create_initial_world(GameState *gs); void initialize(struct GameState *gs, void *entity_arena, size_t entity_arena_size); void destroy(struct GameState *gs); void process_fixed_timestep(GameState *gs); // if is subframe, doesn't always increment the tick. When enough // subframe time has been processed, increments the tick void process(struct GameState *gs, double dt); // does in place Entity *closest_box_to_point_in_radius(struct GameState *gs, cpVect point, double radius, bool (*filter_func)(Entity *)); uint64_t tick(struct GameState *gs); double elapsed_time(GameState *gs); double sun_dist_no_gravity(Entity *sun); void quit_with_popup(const char *message_utf8, const char *title_utf8); // serialization stuff 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; uint32_t git_release_tag; // release tag, unlike version, is about the game version not the serialization verson } SerState; typedef struct SerMaybeFailure { bool failed; int line; const char *expression; } SerMaybeFailure; // all of these return if successful or not size_t ser_size(SerState *ser); SerState init_serializing(GameState *gs, unsigned char *bytes, size_t max_size, Entity *for_player, bool to_disk); SerState init_deserializing(GameState *gs, unsigned char *bytes, size_t max_size, bool from_disk); SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s); SerMaybeFailure ser_client_to_server(SerState *ser, ClientToServer *msg); SerMaybeFailure ser_inputframe(SerState *ser, InputFrame *i); // entities bool is_burning(Entity *missile); Entity *get_entity(struct GameState *gs, EntityID id); Entity *new_entity(struct GameState *gs); EntityID get_id(struct GameState *gs, Entity *e); cpVect entity_pos(Entity *e); bool box_enterable(Entity *box); bool box_interactible(GameState *gs, Player *for_player, Entity *box); void entity_set_rotation(Entity *e, double rot); bool could_learn_from_scanner(Player *for_player, Entity *box); void entity_set_pos(Entity *e, cpVect pos); double entity_rotation(Entity *e); void entity_ensure_in_orbit(GameState *gs, Entity *e); void entity_memory_free(GameState *gs, Entity *e); #define BOX_CHAIN_ITER(gs, cur, starting_box) for (Entity *cur = get_entity(gs, starting_box); cur != NULL; cur = get_entity(gs, cur->next_box)) #define BOXES_ITER(gs, cur, grid_entity_ptr) BOX_CHAIN_ITER(gs, cur, (grid_entity_ptr)->boxes) typedef struct LauncherTarget { bool target_found; double facing_angle; // in global coords } LauncherTarget; LauncherTarget missile_launcher_target(GameState *gs, Entity *launcher); // grid void grid_create(struct GameState *gs, Entity *e); void create_box(struct GameState *gs, Entity *new_box, Entity *grid, cpVect pos, enum BoxType type); Entity *box_grid(Entity *box); cpVect grid_com(Entity *grid); cpVect grid_vel(Entity *grid); cpVect box_vel(Entity *box); cpVect grid_local_to_world(Entity *grid, cpVect local); cpVect grid_world_to_local(Entity *grid, cpVect world); cpVect grid_snapped_box_pos(Entity *grid, cpVect world); // returns the snapped pos in world coords double entity_angular_velocity(Entity *grid); cpVect entity_shape_pos(Entity *box); double box_rotation(Entity *box); // thruster cpVect box_facing_vector(Entity *box); cpVect thruster_force(Entity *box); // debug draw void dbg_drawall(); void dbg_line(cpVect from, cpVect to); void dbg_rect(cpVect center); typedef struct { int __do_not_reference__; } MakeUnreferencable; // used to remove variables from scope typedef struct ServerThreadInfo { ma_mutex info_mutex; const char *world_save; bool should_quit; } ServerThreadInfo; // all the math is static so that it can be defined in each compilation unit its included in typedef struct AABB { double x, y, width, height; } AABB; static inline AABB centered_at(cpVect point, cpVect size) { return (AABB){ .x = point.x - size.x / 2.0f, .y = point.y - size.y / 2.0f, .width = size.x, .height = size.y, }; } static inline bool has_point(AABB aabb, cpVect point) { return point.x > aabb.x && point.x < aabb.x + aabb.width && point.y > aabb.y && point.y < aabb.y + aabb.height; } static inline double cpvprojectval(cpVect vec, cpVect onto) { double length_onto = cpvlength(onto); return cpvdot(vec, onto) / (length_onto * length_onto); } // spins around by theta static inline cpVect cpvspin(cpVect vec, double theta) { return (cpVect){ .x = vec.x * cos(theta) - vec.y * sin(theta), .y = vec.x * sin(theta) + vec.y * cos(theta), }; } // also known as atan2 static inline double cpvangle(cpVect vec) { return atan2(vec.y, vec.x); } typedef struct BoxCentered { cpVect pos; double rotation; cpVect size; // half width and half height, centered on position } BoxCentered; static inline bool box_has_point(BoxCentered box, cpVect point) { cpVect local_point = cpvspin(cpvsub(point, box.pos), -box.rotation); return has_point((AABB){.x = -box.size.x / 2.0, .y = -box.size.y / 2.0, .width = box.size.x, .height = box.size.y}, local_point); } static double sign(double f) { if (f >= 0.0f) return 1.0f; else return -1.0f; } static inline double clamp01(double f) { return fmax(0.0f, fmin(f, 1.0f)); } static inline double clamp(double f, double minimum, double maximum) { if (f < minimum) return minimum; if (f > maximum) return maximum; return f; } static inline double cpvanglediff(cpVect a, cpVect b) { double acos_input = cpvdot(a, b) / (cpvlength(a) * cpvlength(b)); acos_input = clamp(acos_input, -1.0f, 1.0f); flight_assert(acos_input >= -1.0f && acos_input <= 1.0f); return acos(acos_input) * sign(cpvdot(a, b)); } static inline double fract(double f) { return f - floor(f); } static inline double lerp(double a, double b, double f) { return a * (1.0f - f) + (b * f); } static inline double lerp_angle(double p_from, double p_to, double p_weight) { double difference = fmod(p_to - p_from, (float)TAU); double distance = fmod(2.0f * difference, (float)TAU) - difference; return p_from + distance * p_weight; } static inline cpVect cpvfloor(cpVect p) { return (cpVect){floor(p.x), floor(p.y)}; } static inline cpVect cpvfract(cpVect p) { return (cpVect){fract(p.x), fract(p.y)}; } // for random generation static inline double hash11(double p) { p = fract(p * .1031f); p *= p + 33.33f; p *= p + p; return 2.0 * fract(p) - 1.0; } static inline double deg2rad(double deg) { return (deg / 360.0f) * 2.0f * PI; } #define min(X, Y) (((X) < (Y)) ? (X) : (Y)) #define max(X, Y) (((X) > (Y)) ? (X) : (Y))