Serialization refactor, FIX BAD RADAR BUG

main
Cameron Murphy Reikes 2 years ago
parent 1fa9e497d1
commit cb661bd98f

Binary file not shown.

@ -1152,28 +1152,6 @@ void update_from(cpBody *body, struct BodyData *data)
cpBodySetAngularVelocity(body, data->angular_velocity); 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;
uint32_t git_release_tag;
} SerState;
typedef struct SerMaybeFailure
{
bool failed;
int line;
const char *expression;
} SerMaybeFailure;
const static SerMaybeFailure ser_ok = {0}; const static SerMaybeFailure ser_ok = {0};
#define SER_ASSERT(cond) \ #define SER_ASSERT(cond) \
if (!(cond)) \ if (!(cond)) \
@ -1424,6 +1402,7 @@ SerMaybeFailure ser_player(SerState *ser, Player *p)
SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e) SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e)
{ {
SER_VAR(&e->no_save_to_disk); SER_VAR(&e->no_save_to_disk);
SER_VAR(&e->always_visible);
SER_VAR(&e->generation); SER_VAR(&e->generation);
SER_MAYBE_RETURN(ser_f(ser, &e->damage)); SER_MAYBE_RETURN(ser_f(ser, &e->damage));
@ -1561,7 +1540,6 @@ SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e)
SER_VAR(&e->owning_squad); 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->next_box));
SER_MAYBE_RETURN(ser_entityid(ser, &e->prev_box)); SER_MAYBE_RETURN(ser_entityid(ser, &e->prev_box));
SER_VAR(&e->compass_rotation); SER_VAR(&e->compass_rotation);
@ -1853,6 +1831,14 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
return ser_ok; return ser_ok;
} }
// On serialize:
// 1. put struct's data into bytes, for a specific player or not, and to disk or not
// 2. output the number of bytes it took to put the struct's data into bytes
// On deserialize:
// 1. put bytes into a struct for a specific player, from disk or not
// 2. perform operations on those bytes to initialize the struct
// for_this_player can be null then the entire world will be sent // 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) 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)
{ {
@ -1965,60 +1951,47 @@ SerMaybeFailure ser_client_to_server(SerState *ser, ClientToServer *msg)
} }
return ser_ok; return ser_ok;
} }
size_t ser_size(SerState *ser)
{
flight_assert(ser->cursor > 0); // if this fails, haven't serialized anything yet!
return ser->cursor + 1;
}
bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, unsigned char *bytes, size_t *out_len, size_t max_len) SerState init_serializing(GameState *gs, unsigned char *bytes, size_t max_size, Entity *for_player, bool to_disk)
{ {
SerState ser = (SerState){ bool write_varnames = to_disk;
#ifdef WRITE_VARNAMES
write_varnames = true;
#endif
return (SerState){
.bytes = bytes, .bytes = bytes,
.serializing = true, .serializing = true,
.cursor = 0, .cursor = 0,
.max_size = max_len,
.for_player = NULL,
.max_entity_index = gs->cur_next_entity, .max_entity_index = gs->cur_next_entity,
.max_size = max_size,
.for_player = for_player,
.version = VMax - 1, .version = VMax - 1,
.save_or_load_from_disk = to_disk,
.write_varnames = write_varnames,
}; };
#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 init_deserializing(GameState *gs, unsigned char *bytes, size_t max_size, bool from_disk)
{ {
SerState servar = (SerState){ bool has_varnames = from_disk;
#ifdef WRITE_VARNAMES
has_varnames = true;
#endif
return (SerState){
.bytes = bytes, .bytes = bytes,
.serializing = false, .serializing = false,
.cursor = 0, .cursor = 0,
.max_size = max_len, .max_size = max_size,
.max_entity_index = gs->cur_next_entity, .max_entity_index = gs->max_entities,
.save_or_load_from_disk = false, .for_player = NULL,
.save_or_load_from_disk = from_disk,
.write_varnames = has_varnames,
}; };
#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;
}
} }
// filter func null means everything is ok, if it's not null and returns false, that means // filter func null means everything is ok, if it's not null and returns false, that means
@ -2434,10 +2407,19 @@ void create_initial_world(GameState *gs)
entity_set_pos(grid, cpv(1.5, 0.0)); entity_set_pos(grid, cpv(1.5, 0.0));
BOX_AT_TYPE(grid, cpv(0.0, 0.0), BoxExplosive); BOX_AT_TYPE(grid, cpv(0.0, 0.0), BoxExplosive);
BOX_AT_TYPE(grid, cpv(-BOX_SIZE, 0.0), BoxScanner); BOX_AT_TYPE(grid, cpv(-BOX_SIZE, 0.0), BoxScanner);
BOX_AT_TYPE(grid, cpv(-BOX_SIZE, BOX_SIZE), BoxSolarPanel); BOX_AT_TYPE(grid, cpv(-BOX_SIZE*2.0, 0.0), BoxSolarPanel);
BOX_AT_TYPE(grid, cpv(-BOX_SIZE, BOX_SIZE * 2.0), BoxSolarPanel); BOX_AT_TYPE(grid, cpv(-BOX_SIZE*3.0, 0.0), BoxSolarPanel);
entity_ensure_in_orbit(gs, grid);
}
{
Entity *grid = new_entity(gs);
grid_create(gs, grid);
entity_set_pos(grid, cpv(0.0, 10.0));
BOX_AT_TYPE(grid, cpv(0.0, 0.0), BoxHullpiece);
entity_ensure_in_orbit(gs, grid); entity_ensure_in_orbit(gs, grid);
} }
#endif #endif
#if 0 // merge box #if 0 // merge box

@ -14,11 +14,11 @@
#define SOKOL_IMPL #define SOKOL_IMPL
#define SOKOL_D3D11 #define SOKOL_D3D11
#include "sokol_app.h" #include "sokol_app.h"
#include "sokol_args.h"
#include "sokol_gfx.h" #include "sokol_gfx.h"
#include "sokol_glue.h" #include "sokol_glue.h"
#include "sokol_gp.h" #include "sokol_gp.h"
#include "sokol_time.h" #include "sokol_time.h"
#include "sokol_args.h"
#pragma warning(default : 33010) #pragma warning(default : 33010)
#pragma warning(disable : 6262) // warning about using a lot of stack, lol that's how stb image is #pragma warning(disable : 6262) // warning about using a lot of stack, lol that's how stb image is
#define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION
@ -59,17 +59,17 @@ typedef struct KeyPressed
} KeyPressed; } KeyPressed;
static KeyPressed keypressed[MAX_KEYDOWN] = {0}; static KeyPressed keypressed[MAX_KEYDOWN] = {0};
static cpVect mouse_pos = {0}; static cpVect mouse_pos = {0};
static FILE * record_inputs_to = NULL; static FILE *record_inputs_to = NULL;
static bool fullscreened = false; static bool fullscreened = false;
static bool picking_new_boxtype = false; static bool picking_new_boxtype = false;
static double exec_time = 0.0; // cosmetic bouncing, network stats static double exec_time = 0.0; // cosmetic bouncing, network stats
static float iTime = 0.0; // fmodded to 1000, shader trick http://the-witness.net/news/2022/02/a-shader-trick/ static float iTime = 0.0; // fmodded to 1000, shader trick http://the-witness.net/news/2022/02/a-shader-trick/
// for network statistics, printed to logs with F3 // for network statistics, printed to logs with F3
static uint64_t total_bytes_sent = 0; static uint64_t total_bytes_sent = 0;
static uint64_t total_bytes_received = 0; static uint64_t total_bytes_received = 0;
static double dilating_time_factor = 1.0;
static bool build_pressed = false; static bool build_pressed = false;
static double dilating_time_factor = 1.0;
static double time_to_process = 0.0; static double time_to_process = 0.0;
static bool interact_pressed = false; static bool interact_pressed = false;
#define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1) #define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1)
@ -80,6 +80,7 @@ typedef struct MousePressed
uint64_t frame; uint64_t frame;
} MousePressed; } MousePressed;
static MousePressed mousepressed[MAX_MOUSEBUTTON] = {0}; static MousePressed mousepressed[MAX_MOUSEBUTTON] = {0};
static EntityID maybe_inviting_this_player = {0}; static EntityID maybe_inviting_this_player = {0};
static EntityID hovering_this_player = {0}; static EntityID hovering_this_player = {0};
bool confirm_invite_this_player = false; bool confirm_invite_this_player = false;
@ -511,23 +512,23 @@ static void init(void)
// commandline // commandline
{ {
printf( printf(
"Usage: astris.exe [option]=data , the =stuff is required\n" "Usage: astris.exe [option]=data , the =stuff is required\n"
"host - hosts a server locally if exists in commandline, like `astris.exe host=yes`\n" "host - hosts a server locally if exists in commandline, like `astris.exe host=yes`\n"
"record_inputs_to - records inputs to the file specified" "record_inputs_to - records inputs to the file specified");
); if (sargs_exists("host"))
if(sargs_exists("host"))
{ {
server_thread_handle = (void *)_beginthread(server, 0, (void *)&server_info); server_thread_handle = (void *)_beginthread(server, 0, (void *)&server_info);
sapp_set_window_title("Flight Hosting"); sapp_set_window_title("Flight Hosting");
} }
if(sargs_exists("record_inputs_to")) if (sargs_exists("record_inputs_to"))
{ {
const char *filename = sargs_value("record_inputs_to"); const char *filename = sargs_value("record_inputs_to");
if(filename == NULL){ if (filename == NULL)
{
quit_with_popup("Failed to record inputs, filename not specified", "Failed to record inputs"); quit_with_popup("Failed to record inputs, filename not specified", "Failed to record inputs");
} }
fopen_s(&record_inputs_to, filename, "wb"); fopen_s(&record_inputs_to, filename, "wb");
if(record_inputs_to == NULL) if (record_inputs_to == NULL)
{ {
quit_with_popup("Failed to open file to record inputs into", "Failed to record inputs"); quit_with_popup("Failed to open file to record inputs into", "Failed to record inputs");
} }
@ -650,7 +651,7 @@ static void init(void)
quit_with_popup("Couldn't make a shader! Uhhh ooooohhhhhh!!!", "Shader error BONED"); quit_with_popup("Couldn't make a shader! Uhhh ooooohhhhhh!!!", "Shader error BONED");
} }
} }
{ {
sgp_pipeline_desc pip_desc = { sgp_pipeline_desc pip_desc = {
.shader = *lightning_program_shader_desc(sg_query_backend()), .shader = *lightning_program_shader_desc(sg_query_backend()),
@ -1585,8 +1586,12 @@ static void frame(void)
{ {
PROFILE_SCOPE("Deserializing data") PROFILE_SCOPE("Deserializing data")
{ {
server_to_client_deserialize(&msg, decompressed, SerState ser = init_deserializing(&gs, decompressed, decompressed_max_len, false);
decompressed_max_len, false); SerMaybeFailure maybe_fail = ser_server_to_client(&ser, &msg);
if (maybe_fail.failed)
{
Log("Failed to deserialize game state packet line %d %s\n", maybe_fail.line, maybe_fail.expression);
}
applied_gamestate_packet = true; applied_gamestate_packet = true;
} }
my_player_index = msg.your_player; my_player_index = msg.your_player;
@ -1857,9 +1862,10 @@ static void frame(void)
.input_data = &input_queue, .input_data = &input_queue,
}; };
unsigned char serialized[MAX_CLIENT_TO_SERVER] = {0}; unsigned char serialized[MAX_CLIENT_TO_SERVER] = {0};
size_t out_len = 0; SerState ser = init_serializing(&gs, serialized, MAX_CLIENT_TO_SERVER, NULL, false);
if (client_to_server_serialize(&gs, &to_send, serialized, &out_len, SerMaybeFailure maybe_fail = ser_client_to_server(&ser, &to_send);
MAX_CLIENT_TO_SERVER)) size_t out_len = ser_size(&ser);
if (!maybe_fail.failed)
{ {
unsigned char compressed[MAX_CLIENT_TO_SERVER] = {0}; unsigned char compressed[MAX_CLIENT_TO_SERVER] = {0};
char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = {0}; char lzo_working_mem[LZO1X_1_MEM_COMPRESS] = {0};
@ -1885,7 +1891,7 @@ static void frame(void)
} }
else else
{ {
Log("Failed to serialize client to server!\n"); Log("Failed to serialize client to server: %d %s\n", maybe_fail.line, maybe_fail.expression);
} }
ma_mutex_unlock(&send_packets_mutex); ma_mutex_unlock(&send_packets_mutex);
} }
@ -2166,10 +2172,10 @@ static void frame(void)
{ {
sgp_set_image(0, (sg_image){0}); sgp_set_image(0, (sg_image){0});
lightning_uniforms_t uniform = { lightning_uniforms_t uniform = {
.iTime = iTime, .iTime = iTime,
}; };
sgp_set_uniform(&uniform, sizeof(uniform)); sgp_set_uniform(&uniform, sizeof(uniform));
draw_color_rect_centered(entity_pos(b), BOX_SIZE*2.0); draw_color_rect_centered(entity_pos(b), BOX_SIZE * 2.0);
sgp_reset_image(0); sgp_reset_image(0);
} }
} }
@ -2254,7 +2260,7 @@ static void frame(void)
sgp_set_image(0, image_radardot); sgp_set_image(0, image_radardot);
for (int i = 0; i < SCANNER_MAX_POINTS; i++) for (int i = 0; i < SCANNER_MAX_POINTS; i++)
{ {
if (b->scanner_points[i].x != 0 && b->scanner_points[i].y != 0) if (b->scanner_points[i].x != 0 || b->scanner_points[i].y != 0)
{ {
struct ScannerPoint point = b->scanner_points[i]; struct ScannerPoint point = b->scanner_points[i];
switch (point.kind) switch (point.kind)
@ -2593,8 +2599,7 @@ sapp_desc sokol_main(int argc, char *argv[])
{ {
sargs_setup(&(sargs_desc){ sargs_setup(&(sargs_desc){
.argc = argc, .argc = argc,
.argv = argv .argv = argv});
});
stm_setup(); stm_setup();
ma_mutex_init(&server_info.info_mutex); ma_mutex_init(&server_info.info_mutex);

@ -22,6 +22,12 @@
#include "profiling.h" #include "profiling.h"
static void panicquit()
{
flight_assert(false);
exit(-1);
}
// started in a thread from host // started in a thread from host
void server(void *info_raw) void server(void *info_raw)
{ {
@ -39,7 +45,6 @@ void server(void *info_raw)
gs.server_side_computing = true; gs.server_side_computing = true;
Log("Allocated %zu bytes for entities\n", entities_size); Log("Allocated %zu bytes for entities\n", entities_size);
create_initial_world(&gs); create_initial_world(&gs);
// inputs // inputs
@ -56,9 +61,6 @@ void server(void *info_raw)
OpusEncoder *player_encoders[MAX_PLAYERS] = {0}; OpusEncoder *player_encoders[MAX_PLAYERS] = {0};
OpusDecoder *player_decoders[MAX_PLAYERS] = {0}; OpusDecoder *player_decoders[MAX_PLAYERS] = {0};
#ifdef DEBUG_WORLD
world_save_name = NULL;
#endif
if (world_save_name != NULL) if (world_save_name != NULL)
{ {
size_t read_game_data_buffer_size = entities_size; size_t read_game_data_buffer_size = entities_size;
@ -76,13 +78,18 @@ void server(void *info_raw)
if (actual_length <= 1) if (actual_length <= 1)
{ {
Log("Could only read %zu bytes, error: errno %d\n", actual_length, errno); Log("Could only read %zu bytes, error: errno %d\n", actual_length, errno);
exit(-1); panicquit();
} }
Log("Read %zu bytes from save file\n", actual_length); Log("Read %zu bytes from save file\n", actual_length);
ServerToClient msg = (ServerToClient){ ServerToClient msg = (ServerToClient){
.cur_gs = &gs, .cur_gs = &gs,
}; };
server_to_client_deserialize(&msg, read_game_data, actual_length, true); SerState ser = init_deserializing(&gs, read_game_data, actual_length, true);
SerMaybeFailure maybe_fail = ser_server_to_client(&ser, &msg);
if (maybe_fail.failed)
{
Log("Failed to deserialize game world from save file: %d %s\n", maybe_fail.line, maybe_fail.expression);
}
fclose(file); fclose(file);
} }
@ -97,11 +104,10 @@ void server(void *info_raw)
} }
#define BOX_AT(grid, pos) BOX_AT_TYPE(grid, pos, BoxHullpiece) #define BOX_AT(grid, pos) BOX_AT_TYPE(grid, pos, BoxHullpiece)
if (enet_initialize() != 0) if (enet_initialize() != 0)
{ {
fprintf(stderr, "An error occurred while initializing ENet.\n"); fprintf(stderr, "An error occurred while initializing ENet.\n");
exit(-1); panicquit();
} }
ENetAddress address; ENetAddress address;
@ -122,7 +128,7 @@ void server(void *info_raw)
{ {
fprintf(stderr, fprintf(stderr,
"An error occurred while trying to create an ENet server host.\n"); "An error occurred while trying to create an ENet server host.\n");
exit(-1); panicquit();
} }
Log("Serving on port %d...\n", SERVER_PORT); Log("Serving on port %d...\n", SERVER_PORT);
@ -229,9 +235,11 @@ void server(void *info_raw)
if (return_value == LZO_E_OK) if (return_value == LZO_E_OK)
{ {
if (!client_to_server_deserialize(&gs, &received, decompressed, decompressed_max_len)) SerState ser = init_deserializing(&gs, decompressed, decompressed_max_len, false);
SerMaybeFailure maybe_fail = ser_client_to_server(&ser, &received);
if (maybe_fail.failed)
{ {
Log("Bad packet from client %d\n", (int)player_slot); Log("Bad packet from client %d | %d %s\n", (int)player_slot, maybe_fail.line, maybe_fail.expression);
} }
} }
else else
@ -309,8 +317,10 @@ void server(void *info_raw)
ServerToClient msg = (ServerToClient){ ServerToClient msg = (ServerToClient){
.cur_gs = &gs, .cur_gs = &gs,
}; };
size_t out_len = 0; SerState ser = init_serializing(&gs, world_save_buffer, entities_size, NULL, true);
if (server_to_client_serialize(&msg, world_save_buffer, &out_len, entities_size, NULL, true)) SerMaybeFailure maybe_fail = ser_server_to_client(&ser, &msg);
size_t out_len = ser_size(&ser);
if (!maybe_fail.failed)
{ {
FILE *save_file = NULL; FILE *save_file = NULL;
fopen_s(&save_file, (const char *)world_save_name, "wb"); fopen_s(&save_file, (const char *)world_save_name, "wb");
@ -334,7 +344,7 @@ void server(void *info_raw)
} }
else else
{ {
Log("URGENT: FAILED TO SAVE WORLD FILE!\n"); Log("URGENT: FAILED TO SAVE WORLD FILE! Failed at line %d expression %s\n", maybe_fail.line, maybe_fail.expression);
} }
} }
} }
@ -431,13 +441,15 @@ void server(void *info_raw)
.audio_playback_buffer = &buffer_to_play, .audio_playback_buffer = &buffer_to_play,
}; };
size_t len = 0; SerState ser = init_serializing(&gs, bytes_buffer, MAX_SERVER_TO_CLIENT, this_player_entity, false);
if (server_to_client_serialize(&to_send, bytes_buffer, &len, MAX_SERVER_TO_CLIENT, this_player_entity, false)) SerMaybeFailure maybe_fail = ser_server_to_client(&ser, &to_send);
size_t len = ser_size(&ser);
if (!maybe_fail.failed)
{ {
if (len > MAX_SERVER_TO_CLIENT - 8) if (len > MAX_SERVER_TO_CLIENT - 8)
{ {
Log("Too much data quitting!\n"); Log("Too much data quitting!\n");
exit(-1); panicquit();
} }
size_t compressed_len = 0; size_t compressed_len = 0;

@ -253,6 +253,7 @@ typedef struct Entity
bool exists; bool exists;
EntityID next_free_entity; EntityID next_free_entity;
unsigned int generation; 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. bool no_save_to_disk; // stuff generated later on, like player's bodies or space stations that respawn.
@ -310,7 +311,6 @@ typedef struct Entity
bool is_box; bool is_box;
enum BoxType box_type; enum BoxType box_type;
bool is_platonic; // can't be destroyed, unaffected by physical forces bool is_platonic; // can't be destroyed, unaffected by physical forces
bool always_visible; // always serialized to the player. @Robust check if not used
EntityID next_box; // for the grid! EntityID next_box; // for the grid!
EntityID prev_box; // doubly linked so can remove in middle of chain EntityID prev_box; // doubly linked so can remove in middle of chain
enum CompassRotation compass_rotation; enum CompassRotation compass_rotation;
@ -481,11 +481,36 @@ double sun_dist_no_gravity(Entity *sun);
void quit_with_popup(const char *message_utf8, const char *title_utf8); 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 // all of these return if successful or not
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); size_t ser_size(SerState *ser);
bool server_to_client_deserialize(struct ServerToClient *msg, unsigned char *bytes, size_t max_len, bool from_disk); SerState init_serializing(GameState *gs, unsigned char *bytes, size_t max_size, Entity *for_player, bool to_disk);
bool client_to_server_deserialize(GameState *gs, struct ClientToServer *msg, unsigned char *bytes, size_t max_len); SerState init_deserializing(GameState *gs, unsigned char *bytes, size_t max_size, bool from_disk);
bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, unsigned char *bytes, size_t *out_len, size_t max_len); SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s);
SerMaybeFailure ser_client_to_server(SerState *ser, ClientToServer *msg);
// entities // entities
bool is_burning(Entity *missile); bool is_burning(Entity *missile);

Loading…
Cancel
Save