You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

696 lines
21 KiB
C

#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
#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.2f
#define BOX_MASS 1.0f
#define COLLISION_DAMAGE_SCALING 0.15f
#define THRUSTER_FORCE 24.0f
#define THRUSTER_ENERGY_USED_PER_SECOND 0.005f
#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 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 1.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!
#include <math.h> // sqrt and cos vector functions
#include <stdint.h> // tick is unsigned integer
#include <stdio.h> // 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 <chipmunk.h> // unfortunate but needs cpSpace, cpBody, cpShape etc
#include "queue.h"
#include <stdbool.h>
#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 <time.h> // 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,
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 seat_action;
cpVect hand_pos; // local to player transationally but not rotationally
bool dobuild;
enum BoxType build_type;
enum CompassRotation build_rotation;
} InputFrame;
typedef struct Entity
{
bool exists;
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; // must be a box so shape_size can be set appropriately, and serialized
// 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;
// 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;
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)
// 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
// ent:ity pointers can't move around because of how the physics engine handles user data.
// if you really need this, potentially refactor to store entity IDs instead of pointers
// in the shapes and bodies of chipmunk. Would require editing the library I think
Entity *entities;
unsigned int max_entities; // maximum number of entities possible in the entities list
unsigned int cur_next_entity; // next entity to pass on request of a new entity if the free list is empty
EntityID free_list;
} GameState;
#define PLAYERS_ITER(players, cur) \
for (Player *cur = players; cur < players + MAX_PLAYERS; cur++) \
if (cur->connected)
#define PI 3.14159f
#define TAU (PI * 2.0f)
// 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_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_destroy(GameState *gs, Entity *e);
#define BOX_CHAIN_ITER(gs, cur, starting_box) for (Entity *cur = get_entity(gs, starting_box); cur != NULL; cur = get_entity(gs, cur->next_box))
#define BOXES_ITER(gs, cur, grid_entity_ptr) BOX_CHAIN_ITER(gs, cur, (grid_entity_ptr)->boxes)
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 box_create(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 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 fract(p);
}
static inline double deg2rad(double deg)
{
return (deg / 360.0f) * 2.0f * PI;
}