diff --git a/code/game/src/debug_ui.c b/code/game/src/debug_ui.c index 1343f18..1a91e84 100644 --- a/code/game/src/debug_ui.c +++ b/code/game/src/debug_ui.c @@ -42,11 +42,18 @@ static uint8_t is_handle_ctrl_held; static float debug_xpos = DBG_START_XPOS; static float debug_ypos = DBG_START_YPOS; +typedef enum { + L_NONE = 0, + L_SP, + L_MP, +} limit_kind; + typedef struct debug_item { debug_kind kind; char const *name; float name_width; uint8_t skip; + limit_kind limit_to; union { union { @@ -57,7 +64,6 @@ typedef struct debug_item { struct { struct debug_item *items; uint8_t is_collapsed; - uint8_t is_sp_only; } list; struct { @@ -114,8 +120,8 @@ static debug_item items[] = { { .kind = DITEM_END }, }, - .is_sp_only = true, - } + }, + .limit_to = L_SP, }, { .kind = DITEM_LIST, @@ -142,8 +148,27 @@ static debug_item items[] = { }, { .kind = DITEM_END }, }, - .is_sp_only = true, - } + }, + .limit_to = L_SP, + }, + { + .kind = DITEM_LIST, + .name = "conn metrics", + .list = { + .items = (debug_item[]) { + { .kind = DITEM_COND, .on_success = CondClientDisconnected }, + { .kind = DITEM_TEXT, .name = "status", .proc = DrawLiteral, .text = "disconnected" }, + + { .kind = DITEM_COND, .on_success = CondClientConnected }, + { .kind = DITEM_TEXT, .name = "status", .proc = DrawLiteral, .text = "connected" }, + + { .kind = DITEM_COND, .on_success = CondClientConnected }, + { .kind = DITEM_TEXT, .proc = DrawNetworkStats }, + + { .kind = DITEM_END }, + }, + }, + .limit_to = L_MP, }, { .kind = DITEM_LIST, @@ -178,9 +203,9 @@ static debug_item items[] = { { .kind = DITEM_END }, }, - .is_sp_only = true, .is_collapsed = true, - } + }, + .limit_to = L_SP, }, { .kind = DITEM_LIST, @@ -210,6 +235,8 @@ static debug_item items[] = { debug_draw_result debug_draw_list(debug_item *list, float xpos, float ypos, bool is_shadow) { is_shadow_rendered = is_shadow; for (debug_item *it = list; it->kind != DITEM_END; it += 1) { + if (it->limit_to == L_SP && game_get_kind() != GAMEKIND_SINGLE) continue; + if (it->limit_to == L_MP && game_get_kind() == GAMEKIND_SINGLE) continue; switch (it->kind) { case DITEM_GAP: { ypos += DBG_GAP_HEIGHT; @@ -234,18 +261,20 @@ debug_draw_result debug_draw_list(debug_item *list, float xpos, float ypos, bool UIDrawText(it->name, xpos, ypos, DBG_FONT_SIZE, color); ypos += DBG_FONT_SPACING; if (it->list.is_collapsed) break; - if (it->list.is_sp_only && game_get_kind() != GAMEKIND_SINGLE) break; + debug_draw_result res = debug_draw_list(it->list.items, xpos+DBG_LIST_XPOS_OFFSET, ypos, is_shadow); ypos = res.y; }break; case DITEM_TEXT: { - char const *text = TextFormat("%s: ", it->name); - if (it->name_width == 0) { - it->name_width = (float)UIMeasureText(text, DBG_FONT_SIZE); + if (it->name) { + char const *text = TextFormat("%s: ", it->name); + if (it->name_width == 0) { + it->name_width = (float)UIMeasureText(text, DBG_FONT_SIZE); + } + UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, RAYWHITE); + ZPL_ASSERT(it->proc); } - UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, RAYWHITE); - ZPL_ASSERT(it->proc); debug_draw_result res = it->proc(it, xpos + it->name_width, ypos); ypos = res.y; diff --git a/code/game/src/debug_ui_actions.c b/code/game/src/debug_ui_actions.c index 3a41dc3..bf7528c 100644 --- a/code/game/src/debug_ui_actions.c +++ b/code/game/src/debug_ui_actions.c @@ -1,6 +1,7 @@ #include "debug_ui.h" #include "world/blocks.h" #include "items.h" +#include "network.h" void ActExitGame(void) { @@ -269,3 +270,15 @@ uint8_t CondIsWorldRunning(void) { return !world_is_paused(); } + +// NOTE(zaklaus): connection metrics + +uint8_t +CondClientConnected(void) { + return network_client_is_connected(); +} + +uint8_t +CondClientDisconnected(void) { + return !network_client_is_connected(); +} diff --git a/code/game/src/debug_ui_widgets.c b/code/game/src/debug_ui_widgets.c index ed048b4..a5b1429 100644 --- a/code/game/src/debug_ui_widgets.c +++ b/code/game/src/debug_ui_widgets.c @@ -1,6 +1,7 @@ #include "debug_ui.h" #include "raylib.h" #include "platform.h" +#include "network.h" #include "profiler.h" //~ NOTE(zaklaus): helpers @@ -16,7 +17,16 @@ static inline debug_draw_result DrawColoredText(float xpos, float ypos, char const *text, Color color) { ZPL_ASSERT(text); UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, color); - return (debug_draw_result){.x = xpos + UIMeasureText(text, DBG_FONT_SIZE), .y = ypos + DBG_FONT_SPACING}; + + char const *p = text; + uint8_t newlines = 1; + + do { + if (*p == '\n') + ++newlines; + } while (*p++ != 0); + + return (debug_draw_result){.x = xpos + UIMeasureText(text, DBG_FONT_SIZE), .y = ypos + DBG_FONT_SPACING*newlines}; } static inline debug_draw_result @@ -99,3 +109,31 @@ DrawWorldStepSize(debug_item *it, float xpos, float ypos) { (void)it; return DrawFormattedText(xpos, ypos, TextFormat("%d ms", (int16_t)(sim_step_size*1000.f))); } + +// NOTE(zaklaus): network stats +static inline debug_draw_result +DrawNetworkStats(debug_item *it, float xpos, float ypos) { + (void)it; + + network_client_stats s = network_client_fetch_stats(); + +#define _kb(x) ((x) / 1024) + + debug_draw_result r; + r = DrawFormattedText(xpos, ypos, TextFormat("dn total: %d kb", _kb(s.incoming_total))); + r = DrawFormattedText(xpos, r.y, TextFormat("recv total: %lld kb", _kb(s.total_received))); + + r = DrawFormattedText(xpos, r.y, TextFormat("up total: %lld kb", _kb(s.outgoing_total))); + r = DrawFormattedText(xpos, r.y, TextFormat("sent total: %lld kb", _kb(s.total_sent))); + + r = DrawFormattedText(xpos, r.y, TextFormat("dn rate: %.02f kb/sec (%.02f kbit/sec)", _kb(s.incoming_bandwidth), _kb(s.incoming_bandwidth * 8.0f))); + r = DrawFormattedText(xpos, r.y, TextFormat("up rate: %.02f kb/sec (%.02f kbit/sec)", _kb(s.outgoing_bandwidth), _kb(s.outgoing_bandwidth * 8.0f))); + + r = DrawFormattedText(xpos, r.y, TextFormat("packets sent: %lld", s.packets_sent)); + r = DrawFormattedText(xpos, r.y, TextFormat("packets lost: %d (%.02f%%)", s.packets_lost, s.packet_loss)); + + r = DrawFormattedText(xpos, r.y, TextFormat("ping: %d ms", s.ping)); + +#undef _kb + return r; +} diff --git a/code/game/src/game.c b/code/game/src/game.c index d4bcdef..12e8160 100644 --- a/code/game/src/game.c +++ b/code/game/src/game.c @@ -152,7 +152,7 @@ void game_init(game_kind play_mode, uint32_t num_viewers, int32_t seed, uint16_t if (game_mode == GAMEKIND_HEADLESS) { network_server_start(0, 27000); - ecs_set_target_fps(world_ecs(), 60); + //ecs_set_target_fps(world_ecs(), 60); } } diff --git a/code/game/src/network.c b/code/game/src/network.c index ab0a1b4..18f8f57 100644 --- a/code/game/src/network.c +++ b/code/game/src/network.c @@ -98,6 +98,65 @@ int32_t network_client_tick() { bool network_client_is_connected() { return peer ? enet_peer_get_state(peer) == ENET_PEER_STATE_CONNECTED : false; } +network_client_stats +network_client_fetch_stats(void) { + if (!network_client_is_connected()) + return (network_client_stats){0}; + + network_client_stats stats = {0}; + + stats.incoming_total = peer->incomingDataTotal; + stats.total_received = peer->totalDataReceived; + stats.outgoing_total = peer->outgoingDataTotal; + stats.total_sent = peer->totalDataSent; + + static double next_measure = 0.0; + static float incoming_bandwidth = 0.0f; + static float outgoing_bandwidth = 0.0f; + + if (next_measure < zpl_time_rel()) { +#define MAX_RATE_SAMPLES 8 + static uint64_t last_total_sent = 0; + static uint64_t last_total_recv = 0; + static uint64_t rolling_counter = 0; + static uint64_t sent_buffer[MAX_RATE_SAMPLES] = {0}; + static uint64_t recv_buffer[MAX_RATE_SAMPLES] = {0}; + + uint64_t sent_delta = stats.total_sent - last_total_sent; + uint64_t recv_delta = stats.total_received - last_total_recv; + last_total_sent = stats.total_sent; + last_total_recv = stats.total_received; + + sent_buffer[rolling_counter % MAX_RATE_SAMPLES] = sent_delta; + recv_buffer[rolling_counter % MAX_RATE_SAMPLES] = recv_delta; + ++rolling_counter; + + for (int i = 0; i < MAX_RATE_SAMPLES; i++) { + stats.incoming_bandwidth += recv_buffer[i]; + stats.outgoing_bandwidth += sent_buffer[i]; + } + + incoming_bandwidth = stats.incoming_bandwidth /= MAX_RATE_SAMPLES; + outgoing_bandwidth = stats.outgoing_bandwidth /= MAX_RATE_SAMPLES; + + next_measure = zpl_time_rel() + 1.0; + } else { + stats.incoming_bandwidth = incoming_bandwidth; + stats.outgoing_bandwidth = outgoing_bandwidth; + } + + stats.packets_sent = peer->totalPacketsSent; + stats.packets_lost = peer->totalPacketsLost; + + if (stats.packets_sent > 0) { + stats.packet_loss = stats.packets_lost / (float)stats.packets_sent; + } + + stats.ping = peer->roundTripTime; + stats.low_ping = peer->lowestRoundTripTime; + + return stats; +} //~ NOTE(zaklaus): server diff --git a/code/game/src/network.h b/code/game/src/network.h index fa52391..909910f 100644 --- a/code/game/src/network.h +++ b/code/game/src/network.h @@ -11,6 +11,30 @@ int32_t network_client_tick(void); void network_client_update(void *data); bool network_client_is_connected(); +typedef struct { + // NOTE(zaklaus): persistent stats bytes + uint32_t incoming_total; + uint64_t total_received; + uint32_t outgoing_total; + uint64_t total_sent; + + // NOTE(zaklaus): bandwidth (bytes/sec) + float incoming_bandwidth; + float outgoing_bandwidth; + + // NOTE(zaklaus): packet integrity + uint64_t packets_sent; + uint32_t packets_lost; + float packet_loss; + + // NOTE(zaklaus): ping + uint32_t ping; + uint32_t low_ping; +} network_client_stats; + +network_client_stats +network_client_fetch_stats(void); + // NOTE(zaklaus): server int32_t network_server_start(const char *host, uint16_t port); int32_t network_server_stop(void); diff --git a/code/game/src/world/world.c b/code/game/src/world/world.c index a56fe2b..ee61bdf 100644 --- a/code/game/src/world/world.c +++ b/code/game/src/world/world.c @@ -311,9 +311,9 @@ int32_t world_update() { slow_ms = WORLD_TRACKER_UPDATE_MP_SLOW_MS; } - world_tracker_update(0, fast_ms, 2); - world_tracker_update(1, normal_ms, 4); - world_tracker_update(2, slow_ms, 6); + world_tracker_update(0, fast_ms, 1); + world_tracker_update(1, normal_ms, 2); + world_tracker_update(2, slow_ms, 3); debug_replay_update(); return 0;