#1 - quicr module
This commit is contained in:
@@ -26,8 +26,8 @@ target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw::network
|
||||
tl::expected
|
||||
tw::messaging
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
protobuf::libprotobuf
|
||||
tw::chat::lib
|
||||
spdlog::spdlog
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "MessageSession.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "SendChatMessage.hpp"
|
||||
#include "ChatClientError.hpp"
|
||||
#include "models/ChatMessage.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
class ChatClient {
|
||||
tw::MessageSession m_session;
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
std::function<void(ChatMessage)> m_on_message;
|
||||
std::function<void(tl::expected<void, ChatClientError>)> m_on_send_response;
|
||||
|
||||
@@ -22,45 +22,68 @@ tl::expected<void, tw::chat::ChatClientError> from_error_code(mmo::chat::ChatErr
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
namespace {
|
||||
|
||||
std::unique_ptr<tw::msg::MessageEndpoint> create_endpoint() {
|
||||
auto endpoint_r = tw::msg::MessageEndpoint::create();
|
||||
if (!endpoint_r) {
|
||||
throw std::runtime_error("Chat client failed to create an endpoint: " +
|
||||
endpoint_r.error().message());
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
tw::msg::MessageConnection* connect_to_server(tw::msg::MessageEndpoint* endpoint,
|
||||
const std::string& server_address,
|
||||
int16_t port) {
|
||||
auto server_r = endpoint->connect(server_address, port);
|
||||
if (!server_r) {
|
||||
throw std::runtime_error("Chat client failed to connect: " + server_r.error().message());
|
||||
}
|
||||
|
||||
return server_r.value();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ChatClient::ChatClient(const std::string& server_address, int16_t port)
|
||||
: m_session(net::Address(server_address, port))
|
||||
: m_endpoint(create_endpoint())
|
||||
, m_server(connect_to_server(m_endpoint.get(), server_address, port))
|
||||
, m_messages(m_endpoint.get())
|
||||
{
|
||||
m_session.set_handler(CHAT_MESSAGE_BROADCAST_REQUEST, [this](std::span<const std::byte> data) {
|
||||
if (!m_on_message) return;
|
||||
mmo::chat::ChatMessageBroadcastRequest proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
ChatMessage msg;
|
||||
msg.channel_id = proto.channel_id();
|
||||
msg.client_id = proto.sender_id();
|
||||
msg.message = proto.message();
|
||||
msg.timestamp = ChatMessage::Clock::now();
|
||||
m_on_message(std::move(msg));
|
||||
});
|
||||
m_messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
|
||||
[this](msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& proto) {
|
||||
if (!m_on_message) return;
|
||||
ChatMessage msg;
|
||||
msg.channel_id = proto.channel_id();
|
||||
msg.client_id = proto.sender_id();
|
||||
msg.message = proto.message();
|
||||
msg.timestamp = ChatMessage::Clock::now();
|
||||
m_on_message(std::move(msg));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_SEND_MESSAGE_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
if (!m_on_send_response) return;
|
||||
mmo::chat::SendChatMessageResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_send_response(from_error_code(proto.error()));
|
||||
});
|
||||
m_messages.set_handler<mmo::chat::SendChatMessageResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::SendChatMessageResponse& proto) {
|
||||
if (!m_on_send_response) return;
|
||||
m_on_send_response(from_error_code(proto.error()));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_JOIN_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
if (!m_on_join_response) return;
|
||||
mmo::chat::JoinChannelResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
m_messages.set_handler<mmo::chat::JoinChannelResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::JoinChannelResponse& proto) {
|
||||
if (!m_on_join_response) return;
|
||||
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_LEAVE_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
if (!m_on_leave_response) return;
|
||||
mmo::chat::LeaveChannelResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
m_messages.set_handler<mmo::chat::LeaveChannelResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::LeaveChannelResponse& proto) {
|
||||
if (!m_on_leave_response) return;
|
||||
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
}
|
||||
|
||||
void ChatClient::update() {
|
||||
m_session.update();
|
||||
m_endpoint->update();
|
||||
}
|
||||
|
||||
tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage message) {
|
||||
@@ -68,11 +91,7 @@ tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage messag
|
||||
mesg.set_channel_id(message.channel_id);
|
||||
mesg.set_message(message.message);
|
||||
|
||||
std::vector<std::byte> buf(mesg.ByteSizeLong());
|
||||
(void)mesg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
|
||||
|
||||
auto send_r = m_session.send(Message<mmo::chat::SendChatMessageRequest>::value,
|
||||
std::span(buf), true);
|
||||
auto send_r = m_messages.send(m_server, mesg, true);
|
||||
if (!send_r)
|
||||
return tl::make_unexpected(ChatClientError::PermissionDenied);
|
||||
return {};
|
||||
|
||||
@@ -13,8 +13,9 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
tw::chat::service
|
||||
tw::messaging
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::network
|
||||
tw::quicr
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "Chat.pb.h"
|
||||
#include "MessageRegistry.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
@@ -16,108 +16,68 @@ static mmo::chat::ChatErrorCode to_error_code(tl::expected<void, ChatServerError
|
||||
return mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static std::vector<std::byte> serialize(const T& msg) {
|
||||
std::vector<std::byte> buf(msg.ByteSizeLong());
|
||||
(void)msg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
|
||||
return buf;
|
||||
static std::unique_ptr<msg::MessageEndpoint> bind_endpoint(int port) {
|
||||
auto endpoint_r = msg::MessageEndpoint::bind(port);
|
||||
if (!endpoint_r) {
|
||||
throw std::runtime_error("Chat server failed to bind to port " + std::to_string(port) +
|
||||
": " + endpoint_r.error().message());
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
ChatServerController::ChatServerController(int port)
|
||||
: m_endpoint(net::quicr::QuicrEndpoint::create_and_bind(port).value())
|
||||
, m_listener(net::quicr::QuicrConnectionListener::listen(m_endpoint.get()).value())
|
||||
: m_endpoint(bind_endpoint(port))
|
||||
, m_messages(m_endpoint.get())
|
||||
, m_service([this](uint64_t id, const ChatMessage& msg) { broadcast(id, msg); })
|
||||
{
|
||||
m_endpoint->set_on_peer_connected([](msg::PeerId client_id) {
|
||||
spdlog::info("Chat client connected: {}", client_id);
|
||||
});
|
||||
|
||||
register_handlers();
|
||||
spdlog::info("Chat server listening on port {}", port);
|
||||
}
|
||||
|
||||
void ChatServerController::register_handlers() {
|
||||
m_handlers[Message<mmo::chat::SendChatMessageRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::SendChatMessageRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::SendChatMessageRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::SendChatMessageRequest& msg) {
|
||||
mmo::chat::SendChatMessageResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(to_error_code(m_service.send_message(client_id, msg.channel_id(), msg.message())));
|
||||
send_to(client_id, Message<mmo::chat::SendChatMessageResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
|
||||
m_handlers[Message<mmo::chat::JoinChannelRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::JoinChannelRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::JoinChannelRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::JoinChannelRequest& msg) {
|
||||
m_service.join_channel(client_id, msg.channel_id());
|
||||
mmo::chat::JoinChannelResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
|
||||
send_to(client_id, Message<mmo::chat::JoinChannelResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
|
||||
m_handlers[Message<mmo::chat::LeaveChannelRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::LeaveChannelRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::LeaveChannelRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::LeaveChannelRequest& msg) {
|
||||
m_service.leave_channel(client_id, msg.channel_id());
|
||||
mmo::chat::LeaveChannelResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
|
||||
send_to(client_id, Message<mmo::chat::LeaveChannelResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
}
|
||||
|
||||
void ChatServerController::update() {
|
||||
m_endpoint->poll();
|
||||
|
||||
net::quicr::QuicrConnection* conn = nullptr;
|
||||
while ((conn = m_listener->listen())) {
|
||||
m_connections.emplace(conn->self_id(), conn);
|
||||
spdlog::info("Chat client connected: {}", conn->self_id());
|
||||
}
|
||||
|
||||
for (auto& [client_id, conn] : m_connections) {
|
||||
auto r = conn->read_into(m_recv_buf);
|
||||
if (!r || *r == 0) continue;
|
||||
dispatch(client_id, std::span(m_recv_buf.data(), *r));
|
||||
}
|
||||
m_endpoint->update();
|
||||
}
|
||||
|
||||
void ChatServerController::dispatch(uint64_t client_id, std::span<const std::byte> data) {
|
||||
constexpr size_t HEADER = sizeof(uint32_t) * 2;
|
||||
if (data.size() < HEADER) {
|
||||
spdlog::warn("ChatServerController: dropped short datagram ({} bytes)", data.size());
|
||||
return;
|
||||
}
|
||||
uint32_t type{};
|
||||
std::memcpy(&type, data.data(), sizeof(type));
|
||||
|
||||
if (type >= m_handlers.size() || !m_handlers[type]) {
|
||||
spdlog::warn("ChatServerController: no handler for type {}", type);
|
||||
return;
|
||||
}
|
||||
m_handlers[type](client_id, data.subspan(HEADER));
|
||||
}
|
||||
|
||||
void ChatServerController::send_to(uint64_t client_id, uint32_t type,
|
||||
std::span<const std::byte> payload, bool reliable) {
|
||||
auto it = m_connections.find(client_id);
|
||||
if (it == m_connections.end()) return;
|
||||
|
||||
constexpr uint32_t SEQ_NONE = 0;
|
||||
std::vector<std::byte> buf(sizeof(type) + sizeof(SEQ_NONE) + payload.size());
|
||||
std::memcpy(buf.data(), &type, sizeof(type));
|
||||
std::memcpy(buf.data() + sizeof(type), &SEQ_NONE, sizeof(SEQ_NONE));
|
||||
std::memcpy(buf.data() + sizeof(type) + sizeof(SEQ_NONE), payload.data(), payload.size());
|
||||
(void)it->second->send_message(std::span(buf), reliable);
|
||||
}
|
||||
|
||||
void ChatServerController::broadcast(uint64_t client_id, const ChatMessage& msg) {
|
||||
void ChatServerController::broadcast(msg::PeerId client_id, const ChatMessage& msg) {
|
||||
mmo::chat::ChatMessageBroadcastRequest bcast;
|
||||
bcast.set_channel_id(msg.channel_id);
|
||||
bcast.set_sender_id(msg.client_id);
|
||||
bcast.set_message(msg.message);
|
||||
send_to(client_id, Message<mmo::chat::ChatMessageBroadcastRequest>::value,
|
||||
serialize(bcast), true);
|
||||
|
||||
(void)m_messages.send_to(client_id, bcast, true);
|
||||
}
|
||||
|
||||
} // namespace tw::chat
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "ChatService.hpp"
|
||||
#include "protocol/quicr/QuicrEndpoint.hpp"
|
||||
#include "protocol/quicr/QuicrConnectionListener.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
class ChatServerController {
|
||||
static constexpr size_t MAX_TYPES = 32;
|
||||
|
||||
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
|
||||
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
|
||||
std::unordered_map<uint64_t, net::quicr::QuicrConnection*> m_connections;
|
||||
std::vector<std::byte> m_recv_buf{64 * 1024};
|
||||
ChatService m_service;
|
||||
|
||||
std::array<std::function<void(uint64_t, std::span<const std::byte>)>, MAX_TYPES> m_handlers{};
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
ProtobufMessages m_messages;
|
||||
ChatService m_service;
|
||||
|
||||
public:
|
||||
explicit ChatServerController(int port = CHAT_DEFAULT_PORT);
|
||||
@@ -31,10 +21,7 @@ public:
|
||||
|
||||
private:
|
||||
void register_handlers();
|
||||
void dispatch(uint64_t client_id, std::span<const std::byte> data);
|
||||
void send_to(uint64_t client_id, uint32_t type, std::span<const std::byte> payload,
|
||||
bool reliable = false);
|
||||
void broadcast(uint64_t client_id, const ChatMessage& msg);
|
||||
void broadcast(msg::PeerId client_id, const ChatMessage& msg);
|
||||
};
|
||||
|
||||
} // namespace tw::chat
|
||||
|
||||
@@ -5,7 +5,8 @@ add_executable(${PROJECT_NAME} ChatMockClient.cpp)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
tw::protocol
|
||||
tw::messaging
|
||||
tw::message_protocol
|
||||
tw::network
|
||||
tw::quicr
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "Address.hpp"
|
||||
#include "MessageSession.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "MessageRegistry.hpp"
|
||||
#include "Chat.pb.h"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <atomic>
|
||||
@@ -56,30 +57,42 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
tw::MessageSession session(tw::net::Address{std::string{host}, port});
|
||||
auto endpoint_r = tw::msg::MessageEndpoint::create();
|
||||
if (!endpoint_r) {
|
||||
spdlog::error("Failed to create an endpoint: {}", endpoint_r.error().message());
|
||||
return 1;
|
||||
}
|
||||
auto& endpoint = endpoint_r.value();
|
||||
|
||||
auto server_r = endpoint->connect(host, port);
|
||||
if (!server_r) {
|
||||
spdlog::error("Failed to connect: {}", server_r.error().message());
|
||||
return 1;
|
||||
}
|
||||
auto* server = server_r.value();
|
||||
|
||||
tw::ProtobufMessages messages(endpoint.get());
|
||||
|
||||
spdlog::info("Connecting to {}:{}...", host, port);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!session.is_established()) {
|
||||
while (!server->is_established()) {
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
spdlog::error("Connection timed out");
|
||||
return 1;
|
||||
}
|
||||
session.update();
|
||||
endpoint->update();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
spdlog::info("Connected. Joining channel {}...", channel_id);
|
||||
|
||||
session.set_handler(tw::Message<mmo::chat::ChatMessageBroadcastRequest>::value,
|
||||
[](std::span<const std::byte> data) {
|
||||
mmo::chat::ChatMessageBroadcastRequest bcast;
|
||||
bcast.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
|
||||
[](tw::msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& bcast) {
|
||||
std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
|
||||
});
|
||||
|
||||
mmo::chat::JoinChannelRequest join;
|
||||
join.set_channel_id(channel_id);
|
||||
(void)session.request(
|
||||
(void)server->request(
|
||||
tw::Message<mmo::chat::JoinChannelRequest>::value,
|
||||
serialize(join),
|
||||
[channel_id](std::span<const std::byte> data) {
|
||||
@@ -102,7 +115,7 @@ int main(int argc, char* argv[]) {
|
||||
mmo::chat::SendChatMessageRequest msg;
|
||||
msg.set_channel_id(channel_id);
|
||||
msg.set_message(line);
|
||||
(void)session.request(
|
||||
(void)server->request(
|
||||
tw::Message<mmo::chat::SendChatMessageRequest>::value,
|
||||
serialize(msg),
|
||||
[channel_id](std::span<const std::byte> data) {
|
||||
@@ -113,7 +126,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
session.update();
|
||||
endpoint->update();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -4,12 +4,14 @@ add_subdirectory(shaders)
|
||||
|
||||
file(GLOB FILES
|
||||
src/*.cpp
|
||||
src/app/*.cpp
|
||||
src/network/*.cpp
|
||||
src/world/*.cpp
|
||||
src/io/*.cpp
|
||||
src/draw/*.cpp
|
||||
src/draw/RenderPasses/*.cpp
|
||||
src/debug/*.cpp
|
||||
src/debug/metrics/*.cpp
|
||||
src/debug/tools/*.cpp
|
||||
)
|
||||
|
||||
@@ -20,11 +22,14 @@ target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
towards
|
||||
tw::network
|
||||
tw::metrics
|
||||
tw::quicr
|
||||
loft::common
|
||||
loft::base
|
||||
loft_window
|
||||
loft::render_graph
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::serialization
|
||||
tw::gui
|
||||
imgui::imgui
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "ClientArgs.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <charconv>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string_view trim(std::string_view str) {
|
||||
// Skip leading whitespace
|
||||
size_t start = 0;
|
||||
while(start < str.length() && std::isspace(static_cast<unsigned char>(str[start]))) {
|
||||
++start;
|
||||
}
|
||||
|
||||
// Skip trailing whitespace
|
||||
size_t end = str.length();
|
||||
while(end > start && std::isspace(static_cast<unsigned char>(str[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
|
||||
return str.substr(start, end - start);
|
||||
}
|
||||
|
||||
bool is_valid_ipv4(std::string_view ip_str) {
|
||||
// Use inet_pton to validate IPv4 format
|
||||
struct in_addr addr;
|
||||
return inet_pton(AF_INET, std::string(ip_str).c_str(), &addr) == 1;
|
||||
}
|
||||
|
||||
tl::expected<int, std::string> parse_port(std::string_view port_str) {
|
||||
if(port_str.empty()) {
|
||||
return 8080; // Default port
|
||||
}
|
||||
|
||||
int port = 0;
|
||||
const char* end = port_str.data() + port_str.length();
|
||||
auto result = std::from_chars(port_str.data(), end, port);
|
||||
|
||||
// from_chars stops at the first character it cannot use, so a partially
|
||||
// numeric port like "80x" would otherwise be accepted as 80.
|
||||
if(result.ec != std::errc() || result.ptr != end) {
|
||||
return tl::make_unexpected("port must be numeric");
|
||||
}
|
||||
|
||||
if(port < 1 || port > 65535) {
|
||||
return tl::make_unexpected("port must be between 1 and 65535");
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
// Trim whitespace
|
||||
text = trim(text);
|
||||
|
||||
if(text.empty()) {
|
||||
return tl::make_unexpected("address cannot be empty");
|
||||
}
|
||||
|
||||
// Find the colon to split host and port
|
||||
size_t colon_pos = text.rfind(':');
|
||||
|
||||
std::string_view host;
|
||||
std::string_view port_str;
|
||||
|
||||
if(colon_pos == std::string_view::npos) {
|
||||
// No colon found: treat entire string as port or host
|
||||
// If it's all digits, treat as port; otherwise as host (will fail validation)
|
||||
bool all_digits = !text.empty() && std::all_of(text.begin(), text.end(),
|
||||
[](unsigned char c) { return std::isdigit(c); });
|
||||
|
||||
if(all_digits) {
|
||||
host = "127.0.0.1";
|
||||
port_str = text;
|
||||
} else {
|
||||
// Treat as host with no port
|
||||
host = text;
|
||||
port_str = "";
|
||||
}
|
||||
} else {
|
||||
host = text.substr(0, colon_pos);
|
||||
port_str = text.substr(colon_pos + 1);
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if(host.empty()) {
|
||||
return tl::make_unexpected("host cannot be empty");
|
||||
}
|
||||
|
||||
if(!is_valid_ipv4(host)) {
|
||||
return tl::make_unexpected("not a valid IPv4 address");
|
||||
}
|
||||
|
||||
// Parse port
|
||||
auto port_result = parse_port(port_str);
|
||||
if(!port_result) {
|
||||
return tl::make_unexpected(port_result.error());
|
||||
}
|
||||
|
||||
int port = port_result.value();
|
||||
return net::Address(std::optional<std::string>(std::string(host)), port);
|
||||
}
|
||||
|
||||
std::optional<std::string> server_arg(int argc, char** argv) {
|
||||
for(int i = 1; i < argc - 1; ++i) {
|
||||
if(std::string_view(argv[i]) == "--server") {
|
||||
return std::string(argv[i + 1]);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Parse an address string into a network address.
|
||||
*
|
||||
* Accepts "host:port" or a bare port number. Bare port uses 127.0.0.1.
|
||||
* Missing port defaults to 8080. Trims surrounding whitespace.
|
||||
* Validates the host with inet_pton and returns an error string
|
||||
* for non-IPv4 addresses or invalid ports.
|
||||
*/
|
||||
tl::expected<net::Address, std::string> parse_address(std::string_view text);
|
||||
|
||||
/**
|
||||
* Extract the --server argument value from the command line.
|
||||
*
|
||||
* Scans argv for --server and returns the following argument,
|
||||
* or nothing if the flag is absent or has no value.
|
||||
*/
|
||||
std::optional<std::string> server_arg(int argc, char** argv);
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "FavouriteServers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
FavouriteServers::FavouriteServers()
|
||||
: FileAddressList("favourite_servers.txt") {
|
||||
}
|
||||
|
||||
const char* FavouriteServers::name() const {
|
||||
return "Favourites";
|
||||
}
|
||||
|
||||
void FavouriteServers::add(const std::string& entry) {
|
||||
// Check if already present
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
return; // Already present, do nothing
|
||||
}
|
||||
|
||||
// Append at the end
|
||||
m_entries.push_back(entry);
|
||||
|
||||
// Cap at MAX_ENTRIES
|
||||
if(m_entries.size() > MAX_ENTRIES) {
|
||||
m_entries.resize(MAX_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Manages a list of favourite server addresses.
|
||||
*
|
||||
* Persists addresses to a text file, one "ip:port" per line.
|
||||
* Appended in the order they are added, de-duplicated,
|
||||
* capped at 32 entries. File path is
|
||||
* $XDG_CONFIG_HOME/towards/favourite_servers.txt, falling back to
|
||||
* $HOME/.config/towards/favourite_servers.txt. If both variables are
|
||||
* unset, keeps the list in memory only.
|
||||
*/
|
||||
class FavouriteServers : public FileAddressList {
|
||||
static constexpr size_t MAX_ENTRIES = 32;
|
||||
|
||||
public:
|
||||
FavouriteServers();
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
/**
|
||||
* Add an address to the favourites list.
|
||||
* Appended at the end if not already present, capped at 32.
|
||||
* Does not persist to disk; call save() after modifying.
|
||||
*/
|
||||
void add(const std::string& entry) override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string get_config_dir() {
|
||||
// Try XDG_CONFIG_HOME first
|
||||
const char* xdg_config_home = std::getenv("XDG_CONFIG_HOME");
|
||||
if(xdg_config_home && xdg_config_home[0] != '\0') {
|
||||
return std::string(xdg_config_home) + "/towards";
|
||||
}
|
||||
|
||||
// Fall back to $HOME/.config/towards
|
||||
const char* home = std::getenv("HOME");
|
||||
if(home && home[0] != '\0') {
|
||||
return std::string(home) + "/.config/towards";
|
||||
}
|
||||
|
||||
// Both unset
|
||||
return "";
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
FileAddressList::FileAddressList(const std::string& file_name) {
|
||||
std::string config_dir = get_config_dir();
|
||||
|
||||
if(config_dir.empty()) {
|
||||
spdlog::debug("XDG_CONFIG_HOME and HOME not set; address lists will not be persisted");
|
||||
m_can_save = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = config_dir + "/" + file_name;
|
||||
m_can_save = true;
|
||||
}
|
||||
|
||||
void FileAddressList::load() {
|
||||
if(m_path.empty()) {
|
||||
return; // No config path available
|
||||
}
|
||||
|
||||
std::ifstream file(m_path);
|
||||
if(!file.is_open()) {
|
||||
// File doesn't exist or can't be read; this is not an error
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries.clear();
|
||||
std::string line;
|
||||
while(std::getline(file, line)) {
|
||||
// Trim whitespace from the line
|
||||
size_t start = line.find_first_not_of(" \t\r\n");
|
||||
size_t end = line.find_last_not_of(" \t\r\n");
|
||||
|
||||
if(start != std::string::npos) {
|
||||
line = line.substr(start, end - start + 1);
|
||||
if(!line.empty()) {
|
||||
m_entries.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileAddressList::save() const {
|
||||
if(!m_can_save || m_path.empty()) {
|
||||
return; // Cannot save without config path
|
||||
}
|
||||
|
||||
// Create the directory if needed
|
||||
std::filesystem::path config_path(m_path);
|
||||
std::filesystem::path config_dir = config_path.parent_path();
|
||||
|
||||
try {
|
||||
std::filesystem::create_directories(config_dir);
|
||||
} catch(const std::filesystem::filesystem_error&) {
|
||||
// If we can't create the directory, silently fail to save
|
||||
return;
|
||||
}
|
||||
|
||||
// Write entries to file
|
||||
std::ofstream file(m_path);
|
||||
if(!file.is_open()) {
|
||||
return; // Can't open file for writing; silently fail
|
||||
}
|
||||
|
||||
for(const auto& entry : m_entries) {
|
||||
file << entry << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void FileAddressList::remove(const std::string& entry) {
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
m_entries.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>& FileAddressList::entries() const {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "ServerAddressProvider.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Shared behaviour of address lists backed by a file.
|
||||
*
|
||||
* One "ip:port" per line. Resolves file path to
|
||||
* $XDG_CONFIG_HOME/towards/<file_name>, falling back to
|
||||
* $HOME/.config/towards/<file_name>.
|
||||
* add() stays pure virtual: subclasses define their own order.
|
||||
*/
|
||||
class FileAddressList : public ServerAddressProvider {
|
||||
protected:
|
||||
std::vector<std::string> m_entries;
|
||||
std::string m_path;
|
||||
bool m_can_save = false;
|
||||
|
||||
explicit FileAddressList(const std::string& file_name);
|
||||
|
||||
public:
|
||||
void load() override;
|
||||
void save() const override;
|
||||
void remove(const std::string& entry) override;
|
||||
const std::vector<std::string>& entries() const override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindowRegistry.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Bundle of runtime-owned subsystems that the game state needs.
|
||||
*/
|
||||
struct GameContext {
|
||||
tw::World* world;
|
||||
tw::JoltPhysicsWorld* physics_world;
|
||||
tw::drw::WorldRenderer* renderer;
|
||||
tw::io::InputManager* input_manager;
|
||||
tw::dbg::NetworkMetrics* metrics;
|
||||
tw::dbg::DebugWindowRegistry* debug_windows;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "GameState.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
GameState::GameState(GameContext context, std::unique_ptr<tw::net::ServerConnection> connection)
|
||||
: m_context(context),
|
||||
m_connection(std::move(connection)),
|
||||
m_entity_gui(context.world),
|
||||
m_network_gui(*context.metrics)
|
||||
{
|
||||
m_controller = std::make_unique<tw::ClientWorldController>(
|
||||
m_context.input_manager,
|
||||
m_context.world,
|
||||
m_context.physics_world,
|
||||
m_context.renderer,
|
||||
m_connection.get(),
|
||||
m_context.metrics
|
||||
);
|
||||
|
||||
m_context.debug_windows->add(&m_entity_gui);
|
||||
m_context.debug_windows->add(&m_network_gui);
|
||||
}
|
||||
|
||||
GameState::~GameState() {
|
||||
m_context.debug_windows->remove(&m_entity_gui);
|
||||
m_context.debug_windows->remove(&m_network_gui);
|
||||
}
|
||||
|
||||
void GameState::update(double delta_time) {
|
||||
m_controller->update(delta_time);
|
||||
m_context.world->step(delta_time);
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "GameContext.hpp"
|
||||
#include "debug/tools/EntityManagerGui.hpp"
|
||||
#include "debug/tools/NetworkStatsGui.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
#include "world/ClientWorldController.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* The game world state. Owns the connection, world controller, and debug GUIs.
|
||||
* Responsible for updating the game simulation and rendering debug information.
|
||||
*/
|
||||
class GameState {
|
||||
GameContext m_context;
|
||||
std::unique_ptr<tw::net::ServerConnection> m_connection;
|
||||
std::unique_ptr<tw::ClientWorldController> m_controller;
|
||||
tw::dbg::tools::EntityManagerGui m_entity_gui;
|
||||
tw::dbg::tools::NetworkStatsGui m_network_gui;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs the game state with the given context and connection.
|
||||
* The connection must be established before creating the game state.
|
||||
*/
|
||||
GameState(GameContext context, std::unique_ptr<tw::net::ServerConnection> connection);
|
||||
|
||||
/**
|
||||
* Takes the debug panels back out of the menu.
|
||||
*/
|
||||
~GameState();
|
||||
|
||||
/**
|
||||
* Updates the game state: draws debug GUIs, updates the controller,
|
||||
* and steps the world physics.
|
||||
*/
|
||||
void update(double delta_time);
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "LobbyState.hpp"
|
||||
|
||||
#include "ClientArgs.hpp"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
const ImVec4 FAVOURITES_COLOUR{1.0f, 0.8f, 0.2f, 1.0f};
|
||||
const ImVec4 RECENT_COLOUR{0.7f, 0.7f, 0.7f, 1.0f};
|
||||
|
||||
}
|
||||
|
||||
LobbyState::LobbyState(std::optional<tw::net::Address> auto_connect) {
|
||||
m_recent.load();
|
||||
m_favourites.load();
|
||||
|
||||
set_address_input(m_recent.entries().empty()
|
||||
? "127.0.0.1:8080"
|
||||
: m_recent.entries().front());
|
||||
|
||||
if(auto_connect) {
|
||||
begin_connect(*auto_connect);
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::set_address_input(const std::string& address) {
|
||||
std::snprintf(m_address_input, sizeof(m_address_input), "%s", address.c_str());
|
||||
}
|
||||
|
||||
void LobbyState::begin_connect(tw::net::Address address) {
|
||||
m_error.clear();
|
||||
m_connection = std::make_unique<tw::net::ServerConnection>(address);
|
||||
|
||||
auto started = m_connection->start();
|
||||
if(!started) {
|
||||
m_error = started.error().message();
|
||||
m_connection.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::draw_form() {
|
||||
const float input_width = 200.0f;
|
||||
|
||||
ImGui::SetNextItemWidth(input_width);
|
||||
bool submitted = ImGui::InputText("##address", m_address_input, sizeof(m_address_input),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
|
||||
ImGui::SameLine();
|
||||
submitted |= ImGui::Button("Connect");
|
||||
|
||||
if(!submitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto parsed = parse_address(m_address_input);
|
||||
if(parsed) {
|
||||
begin_connect(*parsed);
|
||||
} else {
|
||||
m_error = parsed.error();
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::draw_favourite_toggle(const std::string& entry) {
|
||||
const char* label = m_favourites.contains(entry) ? "[*]" : "[ ]";
|
||||
|
||||
// The label alone would collide between the rows drawn in one frame, so the
|
||||
// entry it acts on is what identifies the button.
|
||||
std::string button_id = std::string(label) + "##fav_" + entry;
|
||||
|
||||
if(ImGui::SmallButton(button_id.c_str())) {
|
||||
toggle_favourite(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::toggle_favourite(const std::string& entry) {
|
||||
if(m_favourites.contains(entry)) {
|
||||
m_favourites.remove(entry);
|
||||
} else {
|
||||
m_favourites.add(entry);
|
||||
}
|
||||
m_favourites.save();
|
||||
}
|
||||
|
||||
void LobbyState::draw_provider(ServerAddressProvider& provider, const ImVec4& header_colour) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, header_colour);
|
||||
ImGui::SeparatorText(provider.name());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// BeginChild is one of the two calls whose End must run even when it
|
||||
// returns false, so the result only decides whether rows are submitted.
|
||||
if(ImGui::BeginChild(provider.name(), ImVec2(0, 120), ImGuiChildFlags_Borders)) {
|
||||
for(const auto& entry : provider.entries()) {
|
||||
draw_favourite_toggle(entry);
|
||||
ImGui::SameLine();
|
||||
|
||||
if(ImGui::Selectable(entry.c_str(), false)) {
|
||||
set_address_input(entry);
|
||||
}
|
||||
|
||||
if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
auto parsed = parse_address(entry);
|
||||
if(parsed) {
|
||||
begin_connect(*parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void LobbyState::draw_status() {
|
||||
if(m_connection) {
|
||||
switch(m_connection->status()) {
|
||||
case tw::net::ConnectionStatus::Idle:
|
||||
break;
|
||||
case tw::net::ConnectionStatus::Connecting: {
|
||||
ImGui::TextUnformatted("Connecting...");
|
||||
ImGui::SameLine();
|
||||
draw_favourite_toggle(m_connection->address().to_string());
|
||||
ImGui::SameLine();
|
||||
if(ImGui::Button("Cancel")) {
|
||||
m_connection.reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tw::net::ConnectionStatus::Connected:
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Connected!");
|
||||
m_recent.add(m_connection->address().to_string());
|
||||
m_recent.save();
|
||||
m_result = LobbyResult{std::move(m_connection)};
|
||||
break;
|
||||
case tw::net::ConnectionStatus::Failed: {
|
||||
std::string failed_msg = "Connection failed: " + m_connection->error();
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "%s", failed_msg.c_str());
|
||||
if(ImGui::Button("Dismiss")) {
|
||||
m_connection.reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(!m_error.empty()) {
|
||||
std::string error_msg = "Error: " + m_error;
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "%s", error_msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::update(double delta_time) {
|
||||
if(m_connection) {
|
||||
m_connection->update();
|
||||
}
|
||||
|
||||
// Center the window
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_FirstUseEver,
|
||||
ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGui::Begin("Lobby", nullptr, ImGuiWindowFlags_NoMove);
|
||||
|
||||
draw_form();
|
||||
draw_provider(m_favourites, FAVOURITES_COLOUR);
|
||||
draw_provider(m_recent, RECENT_COLOUR);
|
||||
ImGui::Separator();
|
||||
draw_status();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
std::optional<LobbyResult> LobbyState::take_result() {
|
||||
// Moving out of an optional leaves it engaged, which would hand the caller
|
||||
// a second, empty result on the next frame.
|
||||
auto result = std::move(m_result);
|
||||
m_result.reset();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
#include "RecentServers.hpp"
|
||||
#include "FavouriteServers.hpp"
|
||||
#include "ServerAddressProvider.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
|
||||
struct ImVec4;
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* The edge out of the lobby: a connection that finished its handshake.
|
||||
*/
|
||||
struct LobbyResult {
|
||||
std::unique_ptr<tw::net::ServerConnection> connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* The lobby screen state. Renders an address input field, recent server list,
|
||||
* and manages a connection attempt in flight.
|
||||
*/
|
||||
class LobbyState {
|
||||
static constexpr size_t ADDRESS_INPUT_SIZE = 64;
|
||||
|
||||
tw::app::RecentServers m_recent;
|
||||
tw::app::FavouriteServers m_favourites;
|
||||
|
||||
/**
|
||||
* Edited in place by the input field, so it has to outlive the frame that
|
||||
* draws it rather than being rebuilt from a string every time.
|
||||
*/
|
||||
char m_address_input[ADDRESS_INPUT_SIZE];
|
||||
|
||||
std::string m_error;
|
||||
std::unique_ptr<tw::net::ServerConnection> m_connection;
|
||||
std::optional<LobbyResult> m_result;
|
||||
|
||||
void begin_connect(tw::net::Address address);
|
||||
void set_address_input(const std::string& address);
|
||||
|
||||
void draw_form();
|
||||
|
||||
/**
|
||||
* Renders one address list. The colour is what tells the lists apart on
|
||||
* screen, so it belongs to the lobby rather than to the list itself.
|
||||
*/
|
||||
void draw_provider(ServerAddressProvider& provider, const ImVec4& header_colour);
|
||||
|
||||
void draw_favourite_toggle(const std::string& entry);
|
||||
void toggle_favourite(const std::string& entry);
|
||||
void draw_status();
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs the lobby state, optionally starting an auto-connect if a
|
||||
* server address is provided.
|
||||
*/
|
||||
explicit LobbyState(std::optional<tw::net::Address> auto_connect);
|
||||
|
||||
/**
|
||||
* Updates the lobby: draws the UI, pumps the connection attempt if one
|
||||
* is in flight.
|
||||
*/
|
||||
void update(double delta_time);
|
||||
|
||||
/**
|
||||
* Returns the transition result if the lobby is done. Moves the result
|
||||
* out, leaving none behind.
|
||||
*/
|
||||
std::optional<LobbyResult> take_result();
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "RecentServers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
RecentServers::RecentServers()
|
||||
: FileAddressList("recent_servers.txt") {
|
||||
}
|
||||
|
||||
const char* RecentServers::name() const {
|
||||
return "Recent";
|
||||
}
|
||||
|
||||
void RecentServers::add(const std::string& entry) {
|
||||
// Remove if already in list (de-duplicate)
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
m_entries.erase(it);
|
||||
}
|
||||
|
||||
// Add to front (most recent first)
|
||||
m_entries.insert(m_entries.begin(), entry);
|
||||
|
||||
// Cap at MAX_ENTRIES
|
||||
if(m_entries.size() > MAX_ENTRIES) {
|
||||
m_entries.resize(MAX_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Manages a list of recently used server addresses.
|
||||
*
|
||||
* Persists addresses to a text file, one "ip:port" per line.
|
||||
* Most recent first, capped at 8 entries. File path is
|
||||
* $XDG_CONFIG_HOME/towards/recent_servers.txt, falling back to
|
||||
* $HOME/.config/towards/recent_servers.txt. If both variables are
|
||||
* unset, keeps the list in memory only.
|
||||
*/
|
||||
class RecentServers : public FileAddressList {
|
||||
static constexpr size_t MAX_ENTRIES = 8;
|
||||
|
||||
public:
|
||||
RecentServers();
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
/**
|
||||
* Add an address to the recents list.
|
||||
* Most recent first, de-duplicated, capped at 8.
|
||||
* Does not persist to disk; call save() after connecting.
|
||||
*/
|
||||
void add(const std::string& entry) override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Abstract interface for server address lists.
|
||||
*
|
||||
* Implementations manage a list of "ip:port" entries, load/save them,
|
||||
* and provide a human-readable name for the UI.
|
||||
*/
|
||||
class ServerAddressProvider {
|
||||
public:
|
||||
virtual ~ServerAddressProvider() = default;
|
||||
|
||||
/** Human-readable list name, shown as the section header in the lobby. */
|
||||
virtual const char* name() const = 0;
|
||||
|
||||
virtual const std::vector<std::string>& entries() const = 0;
|
||||
virtual void load() = 0;
|
||||
virtual void save() const = 0;
|
||||
virtual void add(const std::string& entry) = 0;
|
||||
virtual void remove(const std::string& entry) = 0;
|
||||
|
||||
/**
|
||||
* Check whether an entry exists in the list.
|
||||
* Implemented over entries() — subclasses need not override.
|
||||
*/
|
||||
bool contains(const std::string& entry) const {
|
||||
const auto& vec = entries();
|
||||
for(const auto& e : vec) {
|
||||
if(e == entry) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "DebugUI.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
void DebugUI::draw_dockspace() {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
|
||||
const ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus |
|
||||
ImGuiWindowFlags_NoBackground;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::Begin("##debug_dockspace_host", nullptr, flags);
|
||||
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
// A pass through centre leaves the middle empty until something is docked
|
||||
// there, which is where the world is drawn.
|
||||
ImGui::DockSpace(ImGui::GetID("debug_dockspace"), ImVec2(0.0f, 0.0f),
|
||||
ImGuiDockNodeFlags_PassthruCentralNode);
|
||||
|
||||
if(ImGui::BeginMenuBar()) {
|
||||
m_windows.draw_menu();
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DebugUI::draw_windows() {
|
||||
m_windows.draw_windows();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindowRegistry.hpp"
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* The full screen host the debug panels live in: a menu bar to toggle them and
|
||||
* a dock space to arrange them in. Draws no background of its own, so the world
|
||||
* stays visible underneath.
|
||||
*/
|
||||
class DebugUI {
|
||||
DebugWindowRegistry m_windows;
|
||||
|
||||
public:
|
||||
DebugWindowRegistry& windows() {
|
||||
return m_windows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the host for this frame. Has to run before anything that should be
|
||||
* dockable is drawn, since the dock space has to exist by then.
|
||||
*/
|
||||
void draw_dockspace();
|
||||
|
||||
void draw_windows();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "DebugWindow.hpp"
|
||||
#include <imgui.h>
|
||||
#include <utility>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
DebugWindow::DebugWindow(std::string id, std::string title, std::string category)
|
||||
: m_id(std::move(id)),
|
||||
m_title(std::move(title)),
|
||||
m_category(std::move(category)),
|
||||
m_label(m_title + "##" + m_id)
|
||||
{
|
||||
}
|
||||
|
||||
void DebugWindow::draw() {
|
||||
if(!m_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(ImGui::Begin(m_label.c_str(), &m_open)) {
|
||||
draw_contents();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* A debug panel that can be toggled from the menu bar.
|
||||
*
|
||||
* The frame around a panel is drawn here so every one of them gets the same
|
||||
* close button and docking behaviour; subclasses only fill in the contents.
|
||||
*/
|
||||
class DebugWindow {
|
||||
std::string m_id;
|
||||
std::string m_title;
|
||||
std::string m_category;
|
||||
|
||||
/**
|
||||
* The label handed to the ui, "title##id". Saved positions are keyed by the
|
||||
* whole label, so the visible half can change without losing the layout.
|
||||
*/
|
||||
std::string m_label;
|
||||
|
||||
bool m_open = false;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Fills the panel. Called only while it is open, between begin and end.
|
||||
*/
|
||||
virtual void draw_contents() = 0;
|
||||
|
||||
public:
|
||||
DebugWindow(std::string id, std::string title, std::string category);
|
||||
virtual ~DebugWindow() = default;
|
||||
|
||||
DebugWindow(const DebugWindow&) = delete;
|
||||
DebugWindow& operator=(const DebugWindow&) = delete;
|
||||
|
||||
const std::string& id() const { return m_id; }
|
||||
const std::string& title() const { return m_title; }
|
||||
const std::string& category() const { return m_category; }
|
||||
|
||||
bool is_open() const { return m_open; }
|
||||
void set_open(bool open) { m_open = open; }
|
||||
|
||||
/**
|
||||
* The flag the menu item toggles, and the one the close button clears.
|
||||
*/
|
||||
bool* open_flag() { return &m_open; }
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "DebugWindowRegistry.hpp"
|
||||
#include "DebugWindow.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
void DebugWindowRegistry::add(DebugWindow* window) {
|
||||
auto remembered = m_open_state.find(window->id());
|
||||
if(remembered != m_open_state.end()) {
|
||||
window->set_open(remembered->second);
|
||||
}
|
||||
|
||||
m_windows.push_back(window);
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::remove(DebugWindow* window) {
|
||||
m_open_state[window->id()] = window->is_open();
|
||||
std::erase(m_windows, window);
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::draw_menu() {
|
||||
if(!ImGui::BeginMenu("Windows")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ordered by the first panel that asked for the category, so the menu does
|
||||
// not reshuffle as panels come and go.
|
||||
std::vector<std::string> categories;
|
||||
for(auto* window : m_windows) {
|
||||
if(std::find(categories.begin(), categories.end(), window->category()) == categories.end()) {
|
||||
categories.push_back(window->category());
|
||||
}
|
||||
}
|
||||
|
||||
for(const auto& category : categories) {
|
||||
if(!ImGui::BeginMenu(category.c_str())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(auto* window : m_windows) {
|
||||
if(window->category() == category) {
|
||||
ImGui::MenuItem(window->title().c_str(), nullptr, window->open_flag());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::draw_windows() {
|
||||
for(auto* window : m_windows) {
|
||||
window->draw();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
class DebugWindow;
|
||||
|
||||
/**
|
||||
* The debug panels that exist right now.
|
||||
*
|
||||
* Panels are listed by whatever owns them, for as long as it lives, so the menu
|
||||
* follows what the client is currently doing. Whether a panel was open is kept
|
||||
* here rather than on the panel, since the owner is built again on every
|
||||
* reconnect and the panel would come back closed.
|
||||
*/
|
||||
class DebugWindowRegistry {
|
||||
std::vector<DebugWindow*> m_windows;
|
||||
|
||||
/**
|
||||
* Keyed by panel id, remembered across the panels themselves.
|
||||
*/
|
||||
std::unordered_map<std::string, bool> m_open_state;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Lists a panel, restoring whether it was open last time one with the same
|
||||
* id was listed. Ownership stays with the caller, which has to remove it
|
||||
* again before the panel dies.
|
||||
*/
|
||||
void add(DebugWindow* window);
|
||||
|
||||
void remove(DebugWindow* window);
|
||||
|
||||
/**
|
||||
* One submenu per category, listing every panel in it. Expects to be called
|
||||
* inside a menu bar.
|
||||
*/
|
||||
void draw_menu();
|
||||
|
||||
void draw_windows();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* Per-second history of what the client sends, receives and waits for.
|
||||
*
|
||||
* Traffic arrives as running totals, one reading per tick: sample() keeps the
|
||||
* change since the previous reading, so a bucket sums to the traffic of that
|
||||
* second and its extremes are the quietest and busiest tick within it.
|
||||
* Durations are recorded as they are measured.
|
||||
*/
|
||||
class NetworkMetrics {
|
||||
public:
|
||||
using Interval = std::chrono::seconds;
|
||||
using Series = metrics::MetricSeries<Interval>;
|
||||
|
||||
/** How many seconds of history are kept. */
|
||||
static constexpr size_t DEFAULT_HISTORY = 300;
|
||||
|
||||
/** Running totals as of one tick. */
|
||||
struct Totals {
|
||||
uint64_t bytes_sent = 0;
|
||||
uint64_t bytes_received = 0;
|
||||
uint64_t messages_sent = 0;
|
||||
uint64_t messages_received = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
Series m_bytes_out;
|
||||
Series m_bytes_in;
|
||||
Series m_messages_out;
|
||||
Series m_messages_in;
|
||||
Series m_response_ms;
|
||||
Series m_update_ms;
|
||||
|
||||
Series m_rollbacks;
|
||||
Series m_correction_distance;
|
||||
Series m_ack_lag_frames;
|
||||
Series m_replayed_frames;
|
||||
|
||||
Totals m_previous;
|
||||
bool m_has_previous = false;
|
||||
|
||||
static uint64_t delta(uint64_t current, uint64_t previous) {
|
||||
return current > previous ? current - previous : 0;
|
||||
}
|
||||
|
||||
static double to_millis(std::chrono::nanoseconds elapsed) {
|
||||
return std::chrono::duration<double, std::milli>(elapsed).count();
|
||||
}
|
||||
|
||||
public:
|
||||
explicit NetworkMetrics(size_t history_in_seconds = DEFAULT_HISTORY) :
|
||||
m_bytes_out(history_in_seconds),
|
||||
m_bytes_in(history_in_seconds),
|
||||
m_messages_out(history_in_seconds),
|
||||
m_messages_in(history_in_seconds),
|
||||
m_response_ms(history_in_seconds),
|
||||
m_update_ms(history_in_seconds),
|
||||
m_rollbacks(history_in_seconds),
|
||||
m_correction_distance(history_in_seconds),
|
||||
m_ack_lag_frames(history_in_seconds),
|
||||
m_replayed_frames(history_in_seconds)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Records how much `totals` grew since the previous call. The first call
|
||||
* only remembers where the counters started.
|
||||
*/
|
||||
void sample(const Totals& totals) {
|
||||
if(m_has_previous) {
|
||||
m_bytes_out.push((double)delta(totals.bytes_sent, m_previous.bytes_sent));
|
||||
m_bytes_in.push((double)delta(totals.bytes_received, m_previous.bytes_received));
|
||||
m_messages_out.push((double)delta(totals.messages_sent, m_previous.messages_sent));
|
||||
m_messages_in.push((double)delta(totals.messages_received, m_previous.messages_received));
|
||||
}
|
||||
|
||||
m_previous = totals;
|
||||
m_has_previous = true;
|
||||
}
|
||||
|
||||
/** Time between sending an input and seeing the answer to it. */
|
||||
void record_response_time(std::chrono::nanoseconds elapsed) {
|
||||
m_response_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
/** Time one tick spent moving messages in and out, handlers included. */
|
||||
void record_update_time(std::chrono::nanoseconds elapsed) {
|
||||
m_update_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
const Series& bytes_out() const {
|
||||
return m_bytes_out;
|
||||
}
|
||||
|
||||
const Series& bytes_in() const {
|
||||
return m_bytes_in;
|
||||
}
|
||||
|
||||
const Series& messages_out() const {
|
||||
return m_messages_out;
|
||||
}
|
||||
|
||||
const Series& messages_in() const {
|
||||
return m_messages_in;
|
||||
}
|
||||
|
||||
const Series& response_ms() const {
|
||||
return m_response_ms;
|
||||
}
|
||||
|
||||
const Series& update_ms() const {
|
||||
return m_update_ms;
|
||||
}
|
||||
|
||||
/** Records a rollback event (one sample per rollback). */
|
||||
void record_rollback() {
|
||||
m_rollbacks.push(1.0);
|
||||
}
|
||||
|
||||
/** Records the distance in meters of a position correction. */
|
||||
void record_correction_distance(double meters) {
|
||||
m_correction_distance.push(meters);
|
||||
}
|
||||
|
||||
/** Records how many frames behind the ack is trailing the current frame. */
|
||||
void record_ack_lag(uint32_t frames) {
|
||||
m_ack_lag_frames.push((double)frames);
|
||||
}
|
||||
|
||||
/** Records how many frames were replayed during a rollback. */
|
||||
void record_replayed_frames(uint32_t frames) {
|
||||
m_replayed_frames.push((double)frames);
|
||||
}
|
||||
|
||||
const Series& rollbacks() const {
|
||||
return m_rollbacks;
|
||||
}
|
||||
|
||||
const Series& correction_distance() const {
|
||||
return m_correction_distance;
|
||||
}
|
||||
|
||||
const Series& ack_lag_frames() const {
|
||||
return m_ack_lag_frames;
|
||||
}
|
||||
|
||||
const Series& replayed_frames() const {
|
||||
return m_replayed_frames;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
EntityManagerGui::EntityManagerGui(World* world) :
|
||||
DebugWindow("entity_manager", "Entities", "World"),
|
||||
m_world(world) {
|
||||
|
||||
}
|
||||
@@ -64,9 +65,7 @@ void EntityManagerGui::draw_entity_components() {
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void EntityManagerGui::draw() {
|
||||
ImGui::Begin("Transforms");
|
||||
|
||||
void EntityManagerGui::draw_contents() {
|
||||
ImGui::BeginChild("Entities", ImVec2(0, 260), ImGuiChildFlags_Border);
|
||||
|
||||
ImGui::SeparatorText("Entities");
|
||||
@@ -87,8 +86,6 @@ void EntityManagerGui::draw() {
|
||||
if(m_selected_entity.has_value()) {
|
||||
draw_entity_components();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/ComponentGui.hpp"
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class EntityManagerGui {
|
||||
class EntityManagerGui : public tw::dbg::DebugWindow {
|
||||
private:
|
||||
World* m_world;
|
||||
|
||||
@@ -29,14 +30,15 @@ private:
|
||||
...);
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
entt::entity& selected() {
|
||||
return m_selected;
|
||||
}
|
||||
|
||||
EntityManagerGui(World* world);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2,95 +2,109 @@
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
template<typename T, typename Interval, typename Op = net::SumOp<T>>
|
||||
/**
|
||||
* Plots one series against the seconds behind now, ending at the last second
|
||||
* that has fully elapsed.
|
||||
*
|
||||
* The line follows whichever statistic the series is read with. Reading an
|
||||
* average also shades the quietest and busiest value of each second behind it;
|
||||
* a total has no such range to show, since its buckets already hold every value
|
||||
* of that second added together.
|
||||
*/
|
||||
class MetricWidget {
|
||||
std::string m_name;
|
||||
|
||||
net::BucketMetric<T, Interval, Op>& m_metric;
|
||||
|
||||
using Self = MetricWidget<T, Interval, Op>;
|
||||
|
||||
struct {
|
||||
T constraint_from;
|
||||
T constraint_to;
|
||||
|
||||
T from;
|
||||
T to;
|
||||
} y_axis;
|
||||
|
||||
bool m_is_scrolling = true;
|
||||
|
||||
public:
|
||||
MetricWidget(
|
||||
const std::string& name,
|
||||
net::BucketMetric<T, Interval, Op>& metric
|
||||
) :
|
||||
m_name(name),
|
||||
m_metric(metric)
|
||||
{
|
||||
y_axis = {
|
||||
.constraint_from = 0,
|
||||
.constraint_to = 500,
|
||||
.from = 0,
|
||||
.to = 250
|
||||
};
|
||||
using Series = metrics::MetricSeries<std::chrono::seconds>;
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::string m_unit;
|
||||
const Series* m_series;
|
||||
metrics::MetricField m_field;
|
||||
|
||||
/**
|
||||
* The second in progress is left out: it only holds the part of itself
|
||||
* that has elapsed, so drawing it makes the newest point drop and climb
|
||||
* back once a second.
|
||||
*/
|
||||
static constexpr size_t SKIP_IN_PROGRESS = 1;
|
||||
|
||||
std::vector<double> m_ages;
|
||||
std::vector<double> m_values;
|
||||
std::vector<double> m_lows;
|
||||
std::vector<double> m_highs;
|
||||
|
||||
bool has_range() const {
|
||||
return m_field == metrics::MetricField::Avg;
|
||||
}
|
||||
|
||||
Self& set_x_axis_limits_contraints(T from, T to) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from, to);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& set_y_axis_limits(double from, double to) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& enable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
Self& disable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
|
||||
void draw() {
|
||||
ImGui::PushID(m_name.c_str());
|
||||
|
||||
auto head = m_metric.get_head();
|
||||
auto head_timeline = m_metric.get_head_timeline();
|
||||
|
||||
auto tail = m_metric.get_tail();
|
||||
auto tail_timeline = m_metric.get_tail_timeline();
|
||||
|
||||
static float m_metric_history = 10.0f;
|
||||
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
|
||||
if(m_is_scrolling) {
|
||||
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
|
||||
/** Describes what is drawn, so the numbers always match the line. */
|
||||
void draw_summary() const {
|
||||
if(m_values.empty()) {
|
||||
ImGui::TextUnformatted("no samples yet");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Min: %i", m_metric.min());
|
||||
ImGui::Text("Max: %i", m_metric.max());
|
||||
auto [low, high] = std::minmax_element(m_values.begin(), m_values.end());
|
||||
|
||||
if(ImPlot::BeginPlot(m_name.c_str())) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
double total = 0.0;
|
||||
for(double value : m_values) {
|
||||
total += value;
|
||||
}
|
||||
|
||||
ImPlot::SetupAxes("Time", m_name.c_str(), ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
if(m_is_scrolling) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - m_metric_history, from, ImGuiCond_Always);
|
||||
ImGui::Text("min %.1f %s avg %.1f %s max %.1f %s",
|
||||
*low, m_unit.c_str(),
|
||||
total / (double)m_values.size(), m_unit.c_str(),
|
||||
*high, m_unit.c_str());
|
||||
}
|
||||
|
||||
public:
|
||||
MetricWidget(std::string name, std::string unit, const Series& series, metrics::MetricField field) :
|
||||
m_name(std::move(name)),
|
||||
m_unit(std::move(unit)),
|
||||
m_series(&series),
|
||||
m_field(field)
|
||||
{ }
|
||||
|
||||
/** Draws the last `history_in_seconds` seconds of the series. */
|
||||
void draw(size_t history_in_seconds) {
|
||||
ImGui::PushID(m_name.c_str());
|
||||
|
||||
m_series->linearize(m_ages, m_values, m_field, history_in_seconds, SKIP_IN_PROGRESS);
|
||||
|
||||
if(has_range()) {
|
||||
m_series->linearize(m_ages, m_lows, metrics::MetricField::Min,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
m_series->linearize(m_ages, m_highs, metrics::MetricField::Max,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
}
|
||||
|
||||
draw_summary();
|
||||
|
||||
if(ImPlot::BeginPlot(m_name.c_str(), ImVec2(-1.0f, 150.0f))) {
|
||||
ImPlot::SetupAxes("seconds ago", m_unit.c_str(),
|
||||
ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, -(double)history_in_seconds, 0.0, ImGuiCond_Always);
|
||||
|
||||
const int count = (int)m_values.size();
|
||||
|
||||
if(has_range() && count > 0) {
|
||||
ImPlot::PlotShaded("range", m_ages.data(), m_lows.data(), m_highs.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, m_metric.max() * 2, ImGuiCond_Always);
|
||||
// ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
if(count > 0) {
|
||||
ImPlot::PlotLine(m_name.c_str(), m_ages.data(), m_values.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::PlotLine(m_name.c_str(), head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine(m_name.c_str(), tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/MetricWidget.hpp"
|
||||
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class NetworkStatsGui {
|
||||
private:
|
||||
// MetricWidget<uint32_t, std::chrono::seconds, net::AverageOp<uint32_t>> m_ping_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_outgoing_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_incoming_widget;
|
||||
/**
|
||||
* Panel over everything the client measured about its traffic.
|
||||
*
|
||||
* Traffic is shown as the total of each second, since that is the rate the
|
||||
* connection actually carried. Durations are shown as the average of each
|
||||
* second, with the range behind them.
|
||||
*/
|
||||
class NetworkStatsGui : public tw::dbg::DebugWindow {
|
||||
MetricWidget m_response;
|
||||
MetricWidget m_update;
|
||||
MetricWidget m_bytes_in;
|
||||
MetricWidget m_bytes_out;
|
||||
MetricWidget m_messages_in;
|
||||
MetricWidget m_messages_out;
|
||||
|
||||
MetricWidget m_rollbacks;
|
||||
MetricWidget m_correction_distance;
|
||||
MetricWidget m_ack_lag_frames;
|
||||
MetricWidget m_replayed_frames;
|
||||
|
||||
int m_history_in_seconds = 30;
|
||||
|
||||
protected:
|
||||
void draw_contents() override {
|
||||
ImGui::SliderInt("History", &m_history_in_seconds, 5, 300, "%d s");
|
||||
|
||||
const size_t history = (size_t)m_history_in_seconds;
|
||||
|
||||
m_response.draw(history);
|
||||
m_update.draw(history);
|
||||
m_bytes_in.draw(history);
|
||||
m_bytes_out.draw(history);
|
||||
m_messages_in.draw(history);
|
||||
m_messages_out.draw(history);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Prediction");
|
||||
m_rollbacks.draw(history);
|
||||
m_correction_distance.draw(history);
|
||||
m_ack_lag_frames.draw(history);
|
||||
m_replayed_frames.draw(history);
|
||||
}
|
||||
|
||||
public:
|
||||
// NetworkStatsGui() :
|
||||
// m_ping_widget("Ping", net::NetworkStatsLogger::instance()->ping()),
|
||||
// m_outgoing_widget("Outgoing", net::NetworkStatsLogger::instance()->outgoing()),
|
||||
// m_incoming_widget("Incoming", net::NetworkStatsLogger::instance()->incoming())
|
||||
// {
|
||||
// }
|
||||
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// auto& ping = instance->ping();
|
||||
|
||||
ImGui::Begin("Network Stats");
|
||||
|
||||
/* auto head = ping.get_head();
|
||||
auto head_timeline = ping.get_head_timeline();
|
||||
|
||||
auto tail = ping.get_tail();
|
||||
auto tail_timeline = ping.get_tail_timeline();
|
||||
|
||||
static float ping_history = 10.0f;
|
||||
ImGui::SliderFloat("Ping History", &ping_history,1,30,"%.1f s");
|
||||
|
||||
if(ImPlot::BeginPlot("Ping")) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
|
||||
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - ping_history, from, ImGuiCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 120);
|
||||
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
|
||||
ImPlot::PlotLine("Ping", head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine("Ping", tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
} */
|
||||
|
||||
// m_ping_widget.draw();
|
||||
// m_outgoing_widget.draw();
|
||||
// m_incoming_widget.draw();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
explicit NetworkStatsGui(const NetworkMetrics& metrics) :
|
||||
DebugWindow("network_stats", "Network Stats", "Network"),
|
||||
m_response("Response", "ms", metrics.response_ms(), metrics::MetricField::Avg),
|
||||
m_update("Network update", "ms", metrics.update_ms(), metrics::MetricField::Avg),
|
||||
m_bytes_in("Bytes in", "B/s", metrics.bytes_in(), metrics::MetricField::Sum),
|
||||
m_bytes_out("Bytes out", "B/s", metrics.bytes_out(), metrics::MetricField::Sum),
|
||||
m_messages_in("Messages in", "1/s", metrics.messages_in(), metrics::MetricField::Sum),
|
||||
m_messages_out("Messages out", "1/s", metrics.messages_out(), metrics::MetricField::Sum),
|
||||
m_rollbacks("Rollbacks", "1/s", metrics.rollbacks(), metrics::MetricField::Sum),
|
||||
m_correction_distance("Correction distance", "m", metrics.correction_distance(), metrics::MetricField::Avg),
|
||||
m_ack_lag_frames("Ack lag", "frames", metrics.ack_lag_frames(), metrics::MetricField::Avg),
|
||||
m_replayed_frames("Replayed frames", "frames", metrics.replayed_frames(), metrics::MetricField::Avg)
|
||||
{ }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PacketBacklogGui {
|
||||
private:
|
||||
std::vector<uint32_t> m_buckets;
|
||||
uint32_t m_last_backlog_idx;
|
||||
|
||||
public:
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// size_t size = instance->get_size();
|
||||
|
||||
// if(ImGui::BeginTable("Network Packets", 5)) {
|
||||
// ImGui::TableSetupColumn("Message Type");
|
||||
// ImGui::TableSetupColumn("Time");
|
||||
// ImGui::TableSetupColumn("Is From Us");
|
||||
// ImGui::TableSetupColumn("Target");
|
||||
// ImGui::TableSetupColumn("Size");
|
||||
|
||||
// for(int32_t i = size-1; i >= 0; i--) {
|
||||
// auto& item = instance->get_item(i);
|
||||
// ImGui::PushID(item.timepoint.time_since_epoch().count());
|
||||
|
||||
// ImGui::TableNextRow();
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%i", item.message_type);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(std::format("{}", item.timepoint.time_since_epoch()).c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Checkbox("is_sent_from_us", &item.is_sent_by_us);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(item.target.to_string().c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%ld", item.buffer.size());
|
||||
|
||||
// ImGui::PopID();
|
||||
// }
|
||||
// ImGui::EndTable();
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "ByteBuffer.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
PerformanceStatsGui::PerformanceStatsGui(LockStep& lock_step) :
|
||||
DebugWindow("performance_stats", "Performance", "General"),
|
||||
m_lockstep(lock_step),
|
||||
fps_history(1000),
|
||||
frame_idxs(1000)
|
||||
{
|
||||
}
|
||||
|
||||
void PerformanceStatsGui::draw() {
|
||||
ImGui::Begin("Stats");
|
||||
void PerformanceStatsGui::draw_contents() {
|
||||
|
||||
ImGui::Text("FPS: %ld", m_lockstep.fps());
|
||||
fps_history[fps_history_idx] = m_lockstep.fps();
|
||||
@@ -34,8 +34,6 @@ void PerformanceStatsGui::draw() {
|
||||
ImPlot::PlotLine("FPS", frame_idxs.data(), fps_history.data(), is_plot_filled ? (int)fps_history.size() : (int)fps_history_idx - 1);
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PerformanceStatsGui {
|
||||
class PerformanceStatsGui : public tw::dbg::DebugWindow {
|
||||
private:
|
||||
LockStep& m_lockstep;
|
||||
|
||||
@@ -15,10 +16,11 @@ private:
|
||||
uint32_t frame_idx = 0;
|
||||
bool is_plot_filled = false;
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
PerformanceStatsGui(LockStep& lock_step);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "PlayerReconciler.hpp"
|
||||
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/Transform.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtx/norm.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
|
||||
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
|
||||
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
|
||||
{
|
||||
for (auto& record : m_records) {
|
||||
record.frame = 0;
|
||||
record.valid = false;
|
||||
record.input = glm::vec3(0.0f);
|
||||
record.predicted_position = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::record_input(uint32_t frame, glm::vec3 input) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
m_records[idx].frame = frame;
|
||||
m_records[idx].valid = true;
|
||||
m_records[idx].input = input;
|
||||
}
|
||||
|
||||
void PlayerReconciler::record_prediction(uint32_t frame, glm::vec3 position) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
if (m_records[idx].frame == frame && m_records[idx].valid) {
|
||||
m_records[idx].predicted_position = position;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry,
|
||||
uint32_t current_frame)
|
||||
{
|
||||
if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_last_reconciled_ack = ack_frame;
|
||||
m_last_ack_frame = ack_frame;
|
||||
|
||||
Record& record = m_records[ack_frame % RING_SIZE];
|
||||
const bool has_prediction = record.valid && record.frame == ack_frame;
|
||||
|
||||
// With a prediction to compare against, an answer that already matches costs
|
||||
// nothing further. This is the case almost every frame.
|
||||
if (has_prediction) {
|
||||
float distance = glm::distance(record.predicted_position, authoritative_position);
|
||||
m_last_correction_distance = distance;
|
||||
|
||||
if (distance < kPositionEpsilon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restoring the frame the answer describes keeps everything the simulation
|
||||
// derived from it, so only the character has to be moved. Without a stored
|
||||
// frame there is nothing to restore and the answer is taken as it stands.
|
||||
const bool restored = has_prediction && m_physics->rollback(ack_frame);
|
||||
|
||||
place_character(player, registry, authoritative_position, !restored);
|
||||
replay_from(ack_frame, player, registry, current_frame);
|
||||
|
||||
m_last_replayed_frames = current_frame - 1 - ack_frame;
|
||||
m_rollback_count++;
|
||||
|
||||
spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}",
|
||||
ack_frame, m_last_correction_distance, m_last_replayed_frames, restored);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerReconciler::place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity) {
|
||||
CharacterBody* body = registry->try_get<CharacterBody>(player);
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
|
||||
if (clear_velocity) {
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame) {
|
||||
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
|
||||
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
Transform* transform = registry->try_get<Transform>(player);
|
||||
if (!transform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Frames the ring never saw still need an entry, or the answer to them
|
||||
// arrives with nothing to compare against and forces another correction.
|
||||
Record& replayed = m_records[f % RING_SIZE];
|
||||
replayed.frame = f;
|
||||
replayed.valid = true;
|
||||
replayed.predicted_position = transform->position();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::reset_at(uint32_t frame) {
|
||||
m_last_reconciled_ack = frame;
|
||||
|
||||
for (auto& record : m_records) {
|
||||
record.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <glm/glm.hpp>
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace tw {
|
||||
class JoltPhysicsWorld;
|
||||
}
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
class PlayerReconciler {
|
||||
private:
|
||||
struct Record {
|
||||
uint32_t frame;
|
||||
bool valid;
|
||||
glm::vec3 input;
|
||||
glm::vec3 predicted_position;
|
||||
};
|
||||
|
||||
static constexpr size_t RING_SIZE = 64;
|
||||
static constexpr float kPositionEpsilon = 0.05f;
|
||||
|
||||
tw::JoltPhysicsWorld* m_physics;
|
||||
std::array<Record, RING_SIZE> m_records;
|
||||
|
||||
uint32_t m_last_reconciled_ack = 0;
|
||||
uint64_t m_rollback_count = 0;
|
||||
float m_last_correction_distance = 0.0f;
|
||||
uint32_t m_last_replayed_frames = 0;
|
||||
uint32_t m_last_ack_frame = 0;
|
||||
|
||||
public:
|
||||
PlayerReconciler(tw::JoltPhysicsWorld* physics);
|
||||
|
||||
void record_input(uint32_t frame, glm::vec3 input);
|
||||
void record_prediction(uint32_t frame, glm::vec3 position);
|
||||
|
||||
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Re-simulates `from_frame + 1` up to the newest frame, refreshing the stored
|
||||
* prediction for each. The inputs come from the character itself, so this
|
||||
* works even for frames this ring never recorded.
|
||||
*/
|
||||
void replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
/** Places the character at `position` without disturbing the stored frames. */
|
||||
void place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity);
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Drops every stored frame and treats `frame` as already answered. Used when
|
||||
* the player is placed outright, where nothing recorded before the placement
|
||||
* describes where it now is.
|
||||
*/
|
||||
void reset_at(uint32_t frame);
|
||||
|
||||
uint64_t rollback_count() const { return m_rollback_count; }
|
||||
float last_correction_distance() const { return m_last_correction_distance; }
|
||||
uint32_t last_replayed_frames() const { return m_last_replayed_frames; }
|
||||
uint32_t last_ack_frame() const { return m_last_ack_frame; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "ServerConnection.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
ServerConnection::ServerConnection(Address address) :
|
||||
m_address(address),
|
||||
m_status(ConnectionStatus::Idle),
|
||||
m_started_at(Clock::now()) {
|
||||
}
|
||||
|
||||
tl::expected<void, msg::MessageError> ServerConnection::start() {
|
||||
// Create the endpoint
|
||||
auto endpoint_r = msg::MessageEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = endpoint_r.error().message();
|
||||
return tl::make_unexpected(endpoint_r.error());
|
||||
}
|
||||
|
||||
m_endpoint = std::move(endpoint_r.value());
|
||||
|
||||
// Connect to the server
|
||||
auto server_r = m_endpoint->connect(m_address.ip_string(), m_address.port());
|
||||
if(!server_r) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = server_r.error().message();
|
||||
return tl::make_unexpected(server_r.error());
|
||||
}
|
||||
|
||||
m_server = server_r.value();
|
||||
m_status = ConnectionStatus::Connecting;
|
||||
m_started_at = Clock::now();
|
||||
spdlog::info("Attempting to connect to server at {}", m_address.to_string());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void ServerConnection::update() {
|
||||
if(!m_endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_endpoint->update();
|
||||
|
||||
// Check if the connection has become established
|
||||
if(m_status == ConnectionStatus::Connecting && m_server) {
|
||||
if(m_server->is_established()) {
|
||||
m_status = ConnectionStatus::Connected;
|
||||
spdlog::info("Connected to server at {}", m_address.to_string());
|
||||
} else {
|
||||
// Check for timeout
|
||||
auto elapsed = Clock::now() - m_started_at;
|
||||
if(elapsed >= CONNECT_TIMEOUT) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = "No response from " + m_address.to_string();
|
||||
spdlog::error("Connection timeout to {}", m_address.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionStatus ServerConnection::status() const {
|
||||
return m_status;
|
||||
}
|
||||
|
||||
const std::string& ServerConnection::error() const {
|
||||
return m_error;
|
||||
}
|
||||
|
||||
const Address& ServerConnection::address() const {
|
||||
return m_address;
|
||||
}
|
||||
|
||||
msg::MessageEndpoint* ServerConnection::endpoint() const {
|
||||
return m_endpoint.get();
|
||||
}
|
||||
|
||||
msg::MessageConnection* ServerConnection::server() const {
|
||||
return m_server;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include "message_protocol/MessageConnection.hpp"
|
||||
#include "message_protocol/MessageError.hpp"
|
||||
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* Status of a server connection attempt or established connection.
|
||||
*/
|
||||
enum class ConnectionStatus { Idle, Connecting, Connected, Failed };
|
||||
|
||||
/**
|
||||
* Encapsulates a connection to a game server.
|
||||
*
|
||||
* Owns the endpoint and connection, managing the state of the connection
|
||||
* attempt and providing non-blocking access to send/receive.
|
||||
*/
|
||||
class ServerConnection {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server = nullptr;
|
||||
Address m_address;
|
||||
ConnectionStatus m_status = ConnectionStatus::Idle;
|
||||
std::string m_error;
|
||||
Clock::time_point m_started_at;
|
||||
|
||||
static constexpr std::chrono::seconds CONNECT_TIMEOUT{5};
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs a connection object for the given address, without starting I/O.
|
||||
*/
|
||||
explicit ServerConnection(Address address);
|
||||
|
||||
/**
|
||||
* Starts the connection process by creating an endpoint and connecting to
|
||||
* the server. Returns an error if the endpoint cannot be created.
|
||||
*/
|
||||
tl::expected<void, msg::MessageError> start();
|
||||
|
||||
/**
|
||||
* Updates the connection state: pumps the endpoint, and checks for timeout
|
||||
* or successful connection. Must be called regularly.
|
||||
*/
|
||||
void update();
|
||||
|
||||
ConnectionStatus status() const;
|
||||
const std::string& error() const;
|
||||
const Address& address() const;
|
||||
msg::MessageEndpoint* endpoint() const;
|
||||
msg::MessageConnection* server() const;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include "app/ClientArgs.hpp"
|
||||
#include "Address.hpp"
|
||||
#include "SDLWindow.h"
|
||||
#include "debug/tools/NetworkStatsGui.hpp"
|
||||
#include "debug/tools/PacketBacklogGui.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "debug/tools/EntityManagerGui.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "draw/MeshData.hpp"
|
||||
|
||||
#include "imgui.h"
|
||||
@@ -18,6 +16,8 @@
|
||||
#include "implot_internal.h"
|
||||
#include "world/Transform.hpp"
|
||||
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
#include <SDL_events.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
@@ -32,17 +32,23 @@ std::unique_ptr<lft::win::Window> create_window(const std::string& name, VkExten
|
||||
});
|
||||
}
|
||||
|
||||
int get_port_from_args(int argc, char** argv) {
|
||||
try {
|
||||
if(argc > 1) {
|
||||
return atoi(argv[1]);
|
||||
} else {
|
||||
return 8080;
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
std::println("Could not parse port from arguments, using default.");
|
||||
return 8080;
|
||||
/**
|
||||
* The address to connect to without asking, if one was given on the command
|
||||
* line. A malformed one is reported and dropped, leaving the user in the lobby.
|
||||
*/
|
||||
static std::optional<net::Address> auto_connect_address(int argc, char** argv) {
|
||||
auto argument = app::server_arg(argc, argv);
|
||||
if(!argument) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto parsed = app::parse_address(*argument);
|
||||
if(!parsed) {
|
||||
spdlog::error("Failed to parse server address: {}", parsed.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
Runtime::Runtime(int argc, char** argv) :
|
||||
@@ -52,52 +58,44 @@ Runtime::Runtime(int argc, char** argv) :
|
||||
m_physics_world(&m_world),
|
||||
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
|
||||
m_input_manager(m_window.get()),
|
||||
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }),
|
||||
m_lockstep(60)
|
||||
m_network_metrics(),
|
||||
m_lockstep(60),
|
||||
m_debug_ui(),
|
||||
m_perf_stats(m_lockstep),
|
||||
m_state(std::in_place_type<app::LobbyState>, auto_connect_address(argc, argv))
|
||||
{
|
||||
m_debug_ui.windows().add(&m_perf_stats);
|
||||
}
|
||||
|
||||
// bool Runtime::world_state_packet_handler(uint32_t* p_frame_idx, WorldSnapshotMessage* mesg) {
|
||||
// uint32_t frame_idx = *p_frame_idx;
|
||||
Runtime::~Runtime() {
|
||||
m_debug_ui.windows().remove(&m_perf_stats);
|
||||
}
|
||||
|
||||
// if(mesg->frame_idx < frame_idx) {
|
||||
// return false;
|
||||
// }
|
||||
app::GameContext Runtime::context() {
|
||||
return app::GameContext{
|
||||
&m_world,
|
||||
&m_physics_world,
|
||||
&m_world_renderer,
|
||||
&m_input_manager,
|
||||
&m_network_metrics,
|
||||
&m_debug_ui.windows(),
|
||||
};
|
||||
}
|
||||
|
||||
void Runtime::update_state(double delta_time) {
|
||||
if(auto* lobby = std::get_if<app::LobbyState>(&m_state)) {
|
||||
lobby->update(delta_time);
|
||||
|
||||
// for(int i = 0; i < mesg->player_states.size(); i++) {
|
||||
// if(!m_players.contains(mesg->player_states[i].id)) {
|
||||
// auto mesh = m_world_renderer.add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
|
||||
// const auto entity = m_world.registry().create();
|
||||
// m_players.insert({mesg->player_states[i].id, entity});
|
||||
if(auto result = lobby->take_result()) {
|
||||
m_state.emplace<app::GameState>(context(), std::move(result->connection));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// m_world.registry().emplace<Transform>(entity, Transform(mesg->player_states[i].position));
|
||||
// m_world.registry().emplace<PlayerInfoComponent>(entity,
|
||||
// PlayerInfoComponent(
|
||||
// mesg->player_states[i].id,
|
||||
// mesg->player_states[i].name));
|
||||
// m_world.registry().emplace<drw::Mesh>(entity, mesh);
|
||||
|
||||
// } else {
|
||||
// auto entity = (entt::entity)mesg->player_states[i].id;
|
||||
// auto entity_ts = m_world.registry()
|
||||
// .try_get<Transform>(entity);
|
||||
|
||||
// if(entity_ts) {
|
||||
// entity_ts->transform = glm::translate(glm::mat4(1.0f), mesg->player_states[i].position);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
std::get<app::GameState>(m_state).update(delta_time);
|
||||
}
|
||||
|
||||
void Runtime::run() {
|
||||
dbg::tools::EntityManagerGui entity_manager(&m_world);
|
||||
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
|
||||
dbg::tools::PacketBacklogGui packet_backlog;
|
||||
dbg::tools::NetworkStatsGui network_stats;
|
||||
|
||||
ImPlot::CreateContext();
|
||||
m_is_running = true;
|
||||
|
||||
@@ -110,48 +108,18 @@ void Runtime::run() {
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
// ImGui::DockSpaceOverViewport();
|
||||
|
||||
entity_manager.draw();
|
||||
|
||||
ImGui::Begin("History");
|
||||
|
||||
if(ImGui::BeginTable("historyTable", 2)) {
|
||||
// for(auto key : m_world_controller.position_history().keys()) {
|
||||
// ImGui::TableNextRow();
|
||||
//
|
||||
// // auto v = *m_world_controller.player_history().get(key).value();
|
||||
// // auto input = std::format("{} {} {}", v.x, v.y, v.z);
|
||||
// auto p = *m_world_controller.position_history().get(key).value();
|
||||
// auto position = std::format("{} {} {}", p.x, p.y, p.z);
|
||||
//
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%u", key);
|
||||
// // ImGui::TableNextColumn();
|
||||
// // ImGui::Text(input.c_str());
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(position.c_str());
|
||||
// }
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
bool change_imgui = false;
|
||||
|
||||
m_input_manager.update();
|
||||
if(m_input_manager.is_quit()) {
|
||||
m_is_running = false;
|
||||
}
|
||||
m_world_controller.update(m_lockstep.delta_time());
|
||||
|
||||
perf_stats.draw();
|
||||
packet_backlog.draw();
|
||||
network_stats.draw();
|
||||
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
|
||||
// Anything that docks needs the dock space to already be there, so the
|
||||
// host goes up before the state draws.
|
||||
m_debug_ui.draw_dockspace();
|
||||
|
||||
m_world.step(m_lockstep.delta_time());
|
||||
update_state(m_lockstep.delta_time());
|
||||
|
||||
m_debug_ui.draw_windows();
|
||||
|
||||
m_world_renderer.render();
|
||||
FrameMark;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "app/GameContext.hpp"
|
||||
#include "app/GameState.hpp"
|
||||
#include "app/LobbyState.hpp"
|
||||
#include "debug/DebugUI.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
#include "world/ClientWorldController.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
@@ -17,33 +24,38 @@ namespace tw {
|
||||
class Runtime {
|
||||
private:
|
||||
io::Files m_files;
|
||||
|
||||
std::unique_ptr<lft::win::Window> m_window;
|
||||
|
||||
tw::World m_world;
|
||||
|
||||
JoltPhysicsWorld m_physics_world;
|
||||
|
||||
tw::drw::WorldRenderer m_world_renderer;
|
||||
|
||||
tw::io::InputManager m_input_manager;
|
||||
|
||||
tw::ClientWorldController m_world_controller;
|
||||
|
||||
tw::dbg::NetworkMetrics m_network_metrics;
|
||||
tw::LockStep m_lockstep;
|
||||
|
||||
bool m_is_running;
|
||||
/**
|
||||
* Declared before the state so panels the state owns can still be taken out
|
||||
* of the menu while the state is being torn down.
|
||||
*/
|
||||
tw::dbg::DebugUI m_debug_ui;
|
||||
|
||||
tw::dbg::tools::PerformanceStatsGui m_perf_stats;
|
||||
std::variant<tw::app::LobbyState, tw::app::GameState> m_state;
|
||||
bool m_is_running;
|
||||
std::unordered_map<uint32_t, entt::entity> m_players;
|
||||
|
||||
void send_player_positions();
|
||||
|
||||
app::GameContext context();
|
||||
|
||||
void update_state(double delta_time);
|
||||
|
||||
public:
|
||||
const bool is_running() const {
|
||||
return m_is_running;
|
||||
}
|
||||
|
||||
Runtime(int argc, char** argv);
|
||||
~Runtime();
|
||||
|
||||
void run();
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@ struct CameraData {
|
||||
CameraData(glm::mat4 projection, Transform view) :
|
||||
projection(projection),
|
||||
view(view)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
};
|
||||
|
||||
class Camera {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <chrono>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
|
||||
#include "Entity.pb.h"
|
||||
#include "Login.pb.h"
|
||||
@@ -12,9 +12,7 @@
|
||||
#include "PlayerMove.pb.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "messenger/Messenger.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "messages/PlayerMoveMessage.hpp"
|
||||
#include "metrics/HistoryBuffer.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
@@ -22,6 +20,7 @@
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/WorldEntity.hpp"
|
||||
#include "tw/serial/WorldStateWriter.hpp"
|
||||
#include "network/EntityInterpolation.hpp"
|
||||
|
||||
namespace tw {
|
||||
|
||||
@@ -83,17 +82,6 @@ ClientWorldController::create_entity(const std::string& name, glm::vec3 position
|
||||
return entity;
|
||||
}
|
||||
|
||||
net::TcpStream create_stream(tw::net::Address address) {
|
||||
auto stream = net::TcpStream::connect(address);
|
||||
if(!stream.has_value()) {
|
||||
spdlog::error("Failed to connect to server");
|
||||
throw std::runtime_error("Failed to connect to server");
|
||||
}
|
||||
|
||||
stream.value().set_non_blocking();
|
||||
|
||||
return std::move(stream.value());
|
||||
}
|
||||
|
||||
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
|
||||
if(m_entity_mapping.contains(id)) {
|
||||
@@ -134,33 +122,45 @@ ClientWorldController::ClientWorldController(
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
net::ServerConnection* connection,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
) :
|
||||
m_input_manager(inputs),
|
||||
m_world(world),
|
||||
m_physics_world(physics_world),
|
||||
m_world_renderer(world_renderer),
|
||||
m_player_entity(create_player_entity(world, physics_world, world_renderer)),
|
||||
// m_player_entity(/* create_player_entity(world, physics_world, world_renderer) */),
|
||||
m_player_controller(&world_renderer->camera(), glm::vec3()),
|
||||
m_messenger{address},
|
||||
m_connection(connection),
|
||||
m_messages(connection->endpoint()),
|
||||
m_network_metrics(network_metrics),
|
||||
m_tick_step(20),
|
||||
m_is_connected(false),
|
||||
m_input_send_times(INPUT_SEND_TIME_COUNT),
|
||||
m_position_history_exporter("/home/martin/output.csv"),
|
||||
m_entity_interpolator(&m_world->registry(), m_player_entity, 300)
|
||||
m_entity_interpolator(&m_world->registry(), (entt::entity)0, 300),
|
||||
m_reconciler(physics_world)
|
||||
{
|
||||
m_messenger->set_handler<mmo::LoginResponse>(
|
||||
[&](mmo::LoginResponse* mesg) {
|
||||
if(!m_is_connected) {
|
||||
spdlog::info("Joined the game!");
|
||||
}
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
spdlog::info("Logged in!");
|
||||
});
|
||||
|
||||
m_messenger->set_raw_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[&](std::span<std::byte> data) -> tl::expected<void, net::NetworkError> {
|
||||
m_messages.set_handler<mmo::SetControlledEntity>(
|
||||
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
|
||||
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
|
||||
m_controlled_server_id = mesg.entity_id();
|
||||
try_bind_player_entity();
|
||||
});
|
||||
|
||||
m_connection->endpoint()->set_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[this](msg::PeerId, std::span<const std::byte> data) {
|
||||
|
||||
serial::WorldStateReader reader(data);
|
||||
|
||||
auto header = reader.read_header();
|
||||
|
||||
measure_response_time(header.frame_idx);
|
||||
|
||||
while(reader.has_spawn()) {
|
||||
auto spawn = reader.read_spawn();
|
||||
auto entity = create_entity("test", glm::vec3());
|
||||
@@ -168,7 +168,11 @@ ClientWorldController::ClientWorldController(
|
||||
|
||||
map_server_entity(spawn, entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == spawn) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
while(reader.has_entity()) {
|
||||
@@ -182,7 +186,57 @@ ClientWorldController::ClientWorldController(
|
||||
}
|
||||
|
||||
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
|
||||
if(m_player_entity.has_value() && entity.value() == m_player_entity.value()) {
|
||||
// The entity was created before its position was known, so the
|
||||
// body sits at the origin until the server places it. There is
|
||||
// no predicted history to reconcile against yet.
|
||||
if(!m_player_position_initialized) {
|
||||
m_player_position_initialized = true;
|
||||
snap_player_to(entity.value(), p);
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 position_before = glm::vec3(0.0f);
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(entity.value());
|
||||
if(player_transform) {
|
||||
position_before = player_transform->position();
|
||||
}
|
||||
|
||||
bool reconcile_happened = m_reconciler.reconcile(header.frame_idx, p, entity.value(), &m_world->registry(), m_frame_idx);
|
||||
|
||||
if(reconcile_happened) {
|
||||
// Where the replay actually ended up, which is ahead of the
|
||||
// acked position by the frames that were re-simulated.
|
||||
glm::vec3 position_after = player_transform
|
||||
? player_transform->position()
|
||||
: p;
|
||||
glm::vec3 correction_delta = position_before - position_after;
|
||||
float correction_magnitude = glm::length(correction_delta);
|
||||
if(correction_magnitude > 5.0f) {
|
||||
correction_delta = glm::normalize(correction_delta) * 5.0f;
|
||||
}
|
||||
m_visual_error += correction_delta;
|
||||
|
||||
m_render_curr_position = position_after;
|
||||
m_render_prev_position = position_after;
|
||||
m_tick_accumulator = 0.0;
|
||||
|
||||
m_network_metrics->record_rollback();
|
||||
m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
|
||||
m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
|
||||
}
|
||||
|
||||
if(header.frame_idx != 0) {
|
||||
uint32_t ack_lag = 0;
|
||||
if(m_frame_idx >= header.frame_idx) {
|
||||
ack_lag = m_frame_idx - header.frame_idx;
|
||||
}
|
||||
m_network_metrics->record_ack_lag(ack_lag);
|
||||
}
|
||||
} else {
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
}
|
||||
|
||||
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
|
||||
|
||||
@@ -193,33 +247,111 @@ ClientWorldController::ClientWorldController(
|
||||
}
|
||||
|
||||
// apply_entity_positions();
|
||||
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
m_messenger->set_handler<mmo::EntitySpawnMessage>(
|
||||
[&](mmo::EntitySpawnMessage* mesg) {
|
||||
auto entity = create_entity(mesg->name(), glm::vec3());
|
||||
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
|
||||
auto entity = create_entity(mesg.name(), glm::vec3());
|
||||
|
||||
map_server_entity(mesg->entity_id(), entity);
|
||||
map_server_entity(mesg.entity_id(), entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == mesg.entity_id()) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientWorldController::~ClientWorldController() {
|
||||
}
|
||||
|
||||
void ClientWorldController::try_bind_player_entity() {
|
||||
if(!m_controlled_server_id.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto local_entity = map_from_server_entity(m_controlled_server_id.value());
|
||||
if(!local_entity.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entt::entity entity = local_entity.value();
|
||||
spdlog::info("Binding player entity");
|
||||
|
||||
m_player_entity = entity;
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
|
||||
|
||||
m_world->registry().emplace<CharacterController>(entity, 20.0f);
|
||||
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
|
||||
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
|
||||
position
|
||||
));
|
||||
|
||||
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientWorldController::snap_player_to(entt::entity entity, glm::vec3 position) {
|
||||
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
|
||||
if(body) {
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
if(transform) {
|
||||
transform->set_position(position);
|
||||
}
|
||||
|
||||
m_render_prev_position = position;
|
||||
m_render_curr_position = position;
|
||||
m_tick_accumulator = 0.0;
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
|
||||
// Frames simulated before the player was placed describe a position it never
|
||||
// actually had, so answers to them must not be reconciled against.
|
||||
m_reconciler.reset_at(m_frame_idx);
|
||||
}
|
||||
|
||||
void ClientWorldController::export_entity_history() {
|
||||
}
|
||||
|
||||
void ClientWorldController::measure_response_time(uint32_t frame_idx) {
|
||||
// Snapshots carry frame zero until the server has an input to answer, and
|
||||
// repeat the same frame whenever no newer one arrived in between.
|
||||
if(frame_idx == 0 || frame_idx <= m_last_measured_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything the send times no longer cover, including a frame we never sent,
|
||||
// which underflows into a large distance.
|
||||
if(m_frame_idx - frame_idx >= INPUT_SEND_TIME_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_measured_frame = frame_idx;
|
||||
|
||||
auto sent_at = m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT];
|
||||
m_network_metrics->record_response_time(Clock::now() - sent_at);
|
||||
}
|
||||
|
||||
void ClientWorldController::update(double delta_time) {
|
||||
m_player_controller.update(m_input_manager, delta_time);
|
||||
|
||||
ImGui::Begin("Player Controller");
|
||||
if(m_player_entity.has_value()) {
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity.value());
|
||||
}
|
||||
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity);
|
||||
ImGui::Text("Player count: %ld", m_entity_mapping.size());
|
||||
for(auto mapping : m_entity_mapping) {
|
||||
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
|
||||
@@ -228,34 +360,55 @@ void ClientWorldController::update(double delta_time) {
|
||||
ImGui::End();
|
||||
|
||||
if(m_tick_step.update()) {
|
||||
m_messenger->update();
|
||||
auto network_start = Clock::now();
|
||||
m_connection->update();
|
||||
m_network_metrics->record_update_time(Clock::now() - network_start);
|
||||
|
||||
if(!m_is_connected && false) {
|
||||
return;
|
||||
} else {
|
||||
m_network_metrics->sample({
|
||||
.bytes_sent = m_connection->endpoint()->bytes_sent(),
|
||||
.bytes_received = m_connection->endpoint()->bytes_received(),
|
||||
.messages_sent = m_connection->endpoint()->messages_sent(),
|
||||
.messages_received = m_connection->endpoint()->messages_received()
|
||||
});
|
||||
|
||||
// CharacterController& character = m_world->registry().get<CharacterController>(m_player_entity);
|
||||
// character.set_input(m_frame_idx, m_player_controller.input());
|
||||
{
|
||||
glm::vec3 input = m_player_controller.input();
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
|
||||
if(controller) {
|
||||
controller->set_input(m_frame_idx, input);
|
||||
m_reconciler.record_input(m_frame_idx, input);
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
glm::vec3 true_position = player_transform->position();
|
||||
m_reconciler.record_prediction(m_frame_idx, true_position);
|
||||
|
||||
m_render_prev_position = m_render_curr_position;
|
||||
m_render_curr_position = true_position;
|
||||
m_tick_accumulator = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
mmo::PlayerMoveMessage player_move_message = {};
|
||||
|
||||
player_move_message.set_frame_idx(m_frame_idx);
|
||||
mmo::PlayerInput* player_input = new mmo::PlayerInput();
|
||||
player_input->set_x(m_player_controller.input().x);
|
||||
player_input->set_y(m_player_controller.input().y);
|
||||
player_input->set_z(m_player_controller.input().z);
|
||||
player_input->set_x(input.x);
|
||||
player_input->set_y(input.y);
|
||||
player_input->set_z(input.z);
|
||||
|
||||
player_move_message.set_allocated_input(player_input);
|
||||
auto r = m_messenger->send(player_move_message);
|
||||
auto r = m_messages.send(m_connection->server(), player_move_message, false);
|
||||
|
||||
// CharacterBody& ts = m_world->registry().get<CharacterBody>(m_player_entity);
|
||||
// auto position = ts.m_character->GetPosition();
|
||||
// character.position_history().set(m_frame_idx, glm::vec3(position[0], position[1], position[2]));
|
||||
// EntityInterpolation& interpolation = m_world->registry().get<EntityInterpolation>(m_player_entity);
|
||||
// interpolation.push(std::chrono::steady_clock::now(), glm::vec3(position[0], position[1], position[2]));
|
||||
m_input_send_times[m_frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
|
||||
|
||||
// m_player_controller.set_target(glm::vec3(position[0], position[1], position[2]));
|
||||
//
|
||||
export_entity_history();
|
||||
|
||||
m_frame_idx++;
|
||||
@@ -268,9 +421,27 @@ void ClientWorldController::update(double delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, delta_time);
|
||||
|
||||
m_entity_interpolator.update();
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
m_visual_error *= std::exp(-delta_time * kVisualErrorDecayRate);
|
||||
if(glm::length(m_visual_error) < 0.001f) {
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
}
|
||||
|
||||
m_tick_accumulator += delta_time;
|
||||
float alpha = glm::clamp(
|
||||
static_cast<float>(m_tick_accumulator / JoltPhysicsWorld::FIXED_DELTA_TIME),
|
||||
0.0f, 1.0f
|
||||
);
|
||||
glm::vec3 smoothed_position = glm::mix(m_render_prev_position, m_render_curr_position, alpha) + m_visual_error;
|
||||
player_transform->set_position(smoothed_position);
|
||||
|
||||
m_player_controller.set_target(smoothed_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/gtx/io.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "metrics/HistoryBufferExporter.hpp"
|
||||
@@ -15,7 +14,11 @@
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "world/ThirdPersonPlayerController.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
#include "network/PlayerReconciler.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
class ServerConnection;
|
||||
}
|
||||
|
||||
namespace tw {
|
||||
|
||||
@@ -35,10 +38,14 @@ class ClientWorldController {
|
||||
drw::WorldRenderer* m_world_renderer;
|
||||
JoltPhysicsWorld* m_physics_world;
|
||||
|
||||
entt::entity m_player_entity;
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
std::optional<uint32_t> m_controlled_server_id;
|
||||
ThirdPersonPlayerController m_player_controller;
|
||||
|
||||
std::optional<tw::net::MessageHandler> m_messenger;
|
||||
net::ServerConnection* m_connection;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
dbg::NetworkMetrics* m_network_metrics;
|
||||
|
||||
LockStep m_tick_step;
|
||||
|
||||
@@ -47,15 +54,62 @@ class ClientWorldController {
|
||||
|
||||
glm::vec3 m_input;
|
||||
|
||||
bool m_is_connected;
|
||||
|
||||
std::optional<drw::Mesh> m_mesh;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void try_bind_player_entity();
|
||||
|
||||
/**
|
||||
* Places the player at an authoritative position outright, clearing the
|
||||
* predicted state that led there. Used for the first position the server
|
||||
* sends, which the local simulation has no history to reconcile against.
|
||||
*/
|
||||
void snap_player_to(entt::entity entity, glm::vec3 position);
|
||||
|
||||
/**
|
||||
* Whether the server has placed the player at least once. Entities are
|
||||
* created before their position arrives, so the body starts at the origin
|
||||
* and has to be moved once the first position shows up.
|
||||
*/
|
||||
bool m_player_position_initialized = false;
|
||||
|
||||
/**
|
||||
* When each input was sent, indexed by its frame. Holds the most recent
|
||||
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
|
||||
* unmeasured.
|
||||
*/
|
||||
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
|
||||
|
||||
std::vector<Clock::time_point> m_input_send_times;
|
||||
uint32_t m_last_measured_frame = 0;
|
||||
|
||||
/**
|
||||
* Records how long the answer to `frame_idx` took to arrive, ignoring
|
||||
* frames that were already measured or are too old to still have a send
|
||||
* time.
|
||||
*/
|
||||
void measure_response_time(uint32_t frame_idx);
|
||||
|
||||
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
|
||||
|
||||
net::EntityPositionInterpolator m_entity_interpolator;
|
||||
|
||||
net::PlayerReconciler m_reconciler;
|
||||
|
||||
/**
|
||||
* Visual smoothing for render-rate interpolation between 20 Hz ticks.
|
||||
*/
|
||||
glm::vec3 m_render_prev_position{0.0f};
|
||||
glm::vec3 m_render_curr_position{0.0f};
|
||||
double m_tick_accumulator = 0.0;
|
||||
|
||||
/**
|
||||
* Visual error from reconciliation corrections, decays over time.
|
||||
*/
|
||||
glm::vec3 m_visual_error{0.0f};
|
||||
static constexpr double kVisualErrorDecayRate = 12.0;
|
||||
|
||||
/**
|
||||
* Mapping from the server entity_id to local entity_id
|
||||
* Server might have the same entity under different name
|
||||
@@ -65,8 +119,6 @@ class ClientWorldController {
|
||||
|
||||
entt::entity create_entity(const std::string& name, glm::vec3 position);
|
||||
|
||||
net::MessageHandler create_messenger();
|
||||
|
||||
std::optional<entt::entity> map_from_server_entity(int id);
|
||||
|
||||
void map_server_entity(int server_id, entt::entity local_id);
|
||||
@@ -86,7 +138,8 @@ public:
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
net::ServerConnection* connection,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
);
|
||||
|
||||
~ClientWorldController();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
project(tw_io)
|
||||
|
||||
file(GLOB HEADERS
|
||||
include/*.hpp
|
||||
include/io/*.hpp
|
||||
include/bytebuffer/*.hpp
|
||||
include/exception/*.hpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
add_library(tw::io ALIAS ${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME}
|
||||
INTERFACE FILE_SET HEADERS
|
||||
BASE_DIRS include
|
||||
FILES ${HEADERS})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
${PROJECT_SOURCE_DIR}/include/
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
spdlog::spdlog
|
||||
tl::expected
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
# io
|
||||
|
||||
Transport-agnostic I/O primitives shared by the networking modules.
|
||||
|
||||
Header-only (`tw::io`), namespace `tw::net`.
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `io/Read.hpp`, `io/Write.hpp` | `Read<T>` / `Write<T>` interfaces every stream implements (`read_into`, `write`, `flush`, plus `read_exact` helpers) |
|
||||
| `io/BufferReader.hpp`, `io/BufferWriter.hpp` | Buffering decorators that wrap another `Read<T>` / `Write<T>` |
|
||||
| `bytebuffer/ByteBuffer.hpp` | `RingByteBuffer` — circular buffer over a caller-owned `std::span` |
|
||||
| `bytebuffer/ByteBufferReader.hpp`, `ByteBufferWriter.hpp` | Linear cursor read/write over a `std::span` |
|
||||
| `bytebuffer/ByteBufferCodec.hpp`, `ByteBufferEncoder.hpp`, `ByteBufferDecoder.hpp` | Typed push/pop on top of `RingByteBuffer`, specialize `ByteBufferCodec<T>` for custom types |
|
||||
| `NetworkError.hpp` | errno-backed error type returned by `Read` / `Write` |
|
||||
|
||||
This module owns no sockets and links no transport — it sits below `tw::network`
|
||||
and `tw::quicr` so both can share buffers and stream interfaces without depending
|
||||
on each other.
|
||||
@@ -0,0 +1,33 @@
|
||||
project(tw_message_protocol)
|
||||
|
||||
file(GLOB FILES
|
||||
src/*.cpp
|
||||
)
|
||||
|
||||
file(GLOB HEADERS
|
||||
include/message_protocol/*.hpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} OBJECT ${FILES})
|
||||
add_library(tw::message_protocol ALIAS ${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME}
|
||||
PUBLIC FILE_SET HEADERS
|
||||
BASE_DIRS include
|
||||
FILES ${HEADERS})
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE 1)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/include/
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw::io
|
||||
tw::quicr
|
||||
tl::expected
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
add_subdirectory(tests)
|
||||
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include "message_protocol/MessageError.hpp"
|
||||
#include "message_protocol/MessageHeader.hpp"
|
||||
#include "message_protocol/MessageType.hpp"
|
||||
#include "message_protocol/PeerId.hpp"
|
||||
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::net::quicr {
|
||||
class QuicrConnection;
|
||||
}
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
class MessageDispatcher;
|
||||
|
||||
/**
|
||||
* Sends messages to one peer and routes the ones it sends back.
|
||||
*
|
||||
* Owned by the endpoint that created it and valid until the peer disconnects.
|
||||
* Handlers for message addresses are registered on the endpoint and shared by
|
||||
* every peer; a connection only holds the reply handlers for requests it made
|
||||
* itself.
|
||||
*/
|
||||
class MessageConnection {
|
||||
public:
|
||||
using ReplyHandler = std::function<void(std::span<const std::byte>)>;
|
||||
|
||||
private:
|
||||
struct PendingRequest {
|
||||
ReplyHandler on_reply;
|
||||
std::function<void()> on_timeout;
|
||||
std::chrono::steady_clock::time_point expires_at;
|
||||
};
|
||||
|
||||
net::quicr::QuicrConnection* m_connection;
|
||||
MessageDispatcher* m_dispatcher;
|
||||
PeerId m_peer_id;
|
||||
|
||||
std::vector<std::byte> m_send_buffer;
|
||||
std::unordered_map<uint32_t, PendingRequest> m_pending;
|
||||
uint32_t m_next_seq = 1;
|
||||
uint64_t m_bytes_sent = 0;
|
||||
uint64_t m_messages_sent = 0;
|
||||
uint64_t m_messages_received = 0;
|
||||
|
||||
uint32_t next_seq();
|
||||
|
||||
tl::expected<void, MessageError> send_impl(MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
uint32_t seq,
|
||||
bool reliable);
|
||||
|
||||
public:
|
||||
MessageConnection(PeerId peer_id,
|
||||
net::quicr::QuicrConnection* connection,
|
||||
MessageDispatcher* dispatcher);
|
||||
|
||||
MessageConnection(const MessageConnection&) = delete;
|
||||
MessageConnection& operator=(const MessageConnection&) = delete;
|
||||
|
||||
PeerId peer_id() const {
|
||||
return m_peer_id;
|
||||
}
|
||||
|
||||
uint64_t bytes_sent() const {
|
||||
return m_bytes_sent;
|
||||
}
|
||||
|
||||
/** Messages handed over for sending since the connection was created. */
|
||||
uint64_t messages_sent() const {
|
||||
return m_messages_sent;
|
||||
}
|
||||
|
||||
/** Messages routed from the peer since the connection was created. */
|
||||
uint64_t messages_received() const {
|
||||
return m_messages_received;
|
||||
}
|
||||
|
||||
bool is_established() const;
|
||||
|
||||
tl::expected<void, MessageError> send(MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
bool reliable = false);
|
||||
|
||||
/**
|
||||
* Sends a message the caller has already written a header into, for
|
||||
* callers that build the whole message in a buffer of their own.
|
||||
*/
|
||||
tl::expected<void, MessageError> send_framed(std::span<const std::byte> message,
|
||||
bool reliable = false);
|
||||
|
||||
/**
|
||||
* Sends `body` and calls `on_reply` with the reply carrying the same
|
||||
* sequence number, or `on_timeout` if no reply arrives in time.
|
||||
*/
|
||||
tl::expected<void, MessageError> request(
|
||||
MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
ReplyHandler on_reply,
|
||||
std::chrono::milliseconds timeout = std::chrono::seconds(5),
|
||||
std::function<void()> on_timeout = nullptr,
|
||||
bool reliable = true);
|
||||
|
||||
/**
|
||||
* Reads everything the peer has sent, routing each message, and returns
|
||||
* how many bytes were read. `scratch` is used to hold one message at a
|
||||
* time and may be reused between peers.
|
||||
*/
|
||||
size_t receive(std::span<std::byte> scratch);
|
||||
|
||||
/** Routes one received message to its reply handler, or to the dispatcher. */
|
||||
void on_message(std::span<const std::byte> message);
|
||||
|
||||
/** Fails every request whose reply did not arrive before `now`. */
|
||||
void expire_requests(std::chrono::steady_clock::time_point now);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "message_protocol/MessageType.hpp"
|
||||
#include "message_protocol/PeerId.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
/**
|
||||
* Routes a message body to the handler registered for its address.
|
||||
*
|
||||
* Never inspects the body, so how it is encoded is entirely the caller's
|
||||
* concern. At most one handler may be registered per address.
|
||||
*/
|
||||
class MessageDispatcher {
|
||||
public:
|
||||
using Handler = std::function<void(PeerId, std::span<const std::byte>)>;
|
||||
|
||||
private:
|
||||
std::unordered_map<MessageType, Handler> m_handlers;
|
||||
|
||||
public:
|
||||
/** Registers `handler` for `type`, replacing any handler already there. */
|
||||
void set_handler(MessageType type, Handler handler) {
|
||||
m_handlers[type] = std::move(handler);
|
||||
}
|
||||
|
||||
bool has_handler(MessageType type) const {
|
||||
return m_handlers.contains(type);
|
||||
}
|
||||
|
||||
/** Invokes the handler for `type`. Returns false if there is none. */
|
||||
bool dispatch(PeerId peer, MessageType type, std::span<const std::byte> body) {
|
||||
auto handler = m_handlers.find(type);
|
||||
if(handler == m_handlers.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
handler->second(peer, body);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include "message_protocol/MessageConnection.hpp"
|
||||
#include "message_protocol/MessageDispatcher.hpp"
|
||||
#include "message_protocol/MessageError.hpp"
|
||||
#include "message_protocol/MessageType.hpp"
|
||||
#include "message_protocol/PeerId.hpp"
|
||||
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::net::quicr {
|
||||
class QuicrEndpoint;
|
||||
class QuicrConnectionListener;
|
||||
}
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
/**
|
||||
* Owns the connections to every peer and the handlers shared between them.
|
||||
*
|
||||
* An endpoint created with bind() accepts incoming peers; either kind may
|
||||
* connect() outwards, so one endpoint can serve peers and reach out to others
|
||||
* at the same time.
|
||||
*
|
||||
* update() must be called regularly. Nothing is received and no request ever
|
||||
* times out between calls.
|
||||
*/
|
||||
class MessageEndpoint {
|
||||
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
|
||||
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
|
||||
|
||||
MessageDispatcher m_dispatcher;
|
||||
|
||||
std::unordered_map<PeerId, std::unique_ptr<MessageConnection>> m_peers;
|
||||
std::function<void(PeerId)> m_on_peer_connected;
|
||||
PeerId m_next_peer_id = 1;
|
||||
|
||||
std::vector<std::byte> m_receive_buffer;
|
||||
uint64_t m_bytes_received = 0;
|
||||
|
||||
explicit MessageEndpoint(std::unique_ptr<net::quicr::QuicrEndpoint> endpoint);
|
||||
|
||||
MessageConnection* add_peer(net::quicr::QuicrConnection* connection);
|
||||
void accept_peers();
|
||||
void receive();
|
||||
|
||||
public:
|
||||
~MessageEndpoint();
|
||||
|
||||
MessageEndpoint(const MessageEndpoint&) = delete;
|
||||
MessageEndpoint& operator=(const MessageEndpoint&) = delete;
|
||||
|
||||
/** Creates an endpoint that only connects outwards. */
|
||||
static tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> create();
|
||||
|
||||
/** Creates an endpoint that also accepts peers on `port`. */
|
||||
static tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> bind(int port);
|
||||
|
||||
tl::expected<MessageConnection*, MessageError> connect(const std::string& host, int port);
|
||||
|
||||
/** Registers `handler` for every peer. */
|
||||
void set_handler(MessageType type, MessageDispatcher::Handler handler) {
|
||||
m_dispatcher.set_handler(type, std::move(handler));
|
||||
}
|
||||
|
||||
MessageDispatcher& dispatcher() {
|
||||
return m_dispatcher;
|
||||
}
|
||||
|
||||
/** Receives pending messages, accepts new peers and times out requests. */
|
||||
void update();
|
||||
|
||||
MessageConnection* peer(PeerId id);
|
||||
|
||||
/**
|
||||
* Calls `handler` for each peer that connects or is accepted, before any
|
||||
* of that peer's messages are dispatched.
|
||||
*/
|
||||
void set_on_peer_connected(std::function<void(PeerId)> handler) {
|
||||
m_on_peer_connected = std::move(handler);
|
||||
}
|
||||
|
||||
std::vector<MessageConnection*> peers() const;
|
||||
|
||||
void broadcast(MessageType type, std::span<const std::byte> body, bool reliable = false);
|
||||
|
||||
/** Bytes received since the endpoint was created. */
|
||||
uint64_t bytes_received() const {
|
||||
return m_bytes_received;
|
||||
}
|
||||
|
||||
/** Bytes handed to every peer for sending since the endpoint was created. */
|
||||
uint64_t bytes_sent() const;
|
||||
|
||||
/** Messages handed to every peer for sending since the endpoint was created. */
|
||||
uint64_t messages_sent() const;
|
||||
|
||||
/** Messages routed from every peer since the endpoint was created. */
|
||||
uint64_t messages_received() const;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
enum class MessageErrorType {
|
||||
NotConnected,
|
||||
SendFailed,
|
||||
BindFailed,
|
||||
ConnectFailed,
|
||||
};
|
||||
|
||||
struct MessageError {
|
||||
MessageErrorType type;
|
||||
std::string detail;
|
||||
|
||||
explicit MessageError(MessageErrorType type, std::string detail = {}) :
|
||||
type(type),
|
||||
detail(std::move(detail)) {
|
||||
}
|
||||
|
||||
std::string message() const {
|
||||
std::string text;
|
||||
switch(type) {
|
||||
case MessageErrorType::NotConnected: text = "Not connected to the peer"; break;
|
||||
case MessageErrorType::SendFailed: text = "Failed to send the message"; break;
|
||||
case MessageErrorType::BindFailed: text = "Failed to bind the endpoint"; break;
|
||||
case MessageErrorType::ConnectFailed: text = "Failed to connect to the peer"; break;
|
||||
}
|
||||
|
||||
return detail.empty() ? text : text + ": " + detail;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "message_protocol/MessageType.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
/**
|
||||
* Fixed-size prefix carried by every message.
|
||||
*
|
||||
* `seq` correlates a reply with the request that produced it. SEQ_NONE marks a
|
||||
* message that expects no reply, which is the common case.
|
||||
*/
|
||||
struct MessageHeader {
|
||||
static constexpr uint32_t SEQ_NONE = 0;
|
||||
static constexpr size_t SIZE = sizeof(MessageType) + sizeof(uint32_t);
|
||||
|
||||
MessageType type = 0;
|
||||
uint32_t seq = SEQ_NONE;
|
||||
|
||||
/** Writes the header at the start of `target`, which must hold SIZE bytes. */
|
||||
void encode(std::span<std::byte> target) const {
|
||||
std::memcpy(target.data(), &type, sizeof(type));
|
||||
std::memcpy(target.data() + sizeof(type), &seq, sizeof(seq));
|
||||
}
|
||||
|
||||
/** Reads a header from the start of `source`, or nothing if it is too short. */
|
||||
static std::optional<MessageHeader> decode(std::span<const std::byte> source) {
|
||||
if(source.size() < SIZE) {
|
||||
return {};
|
||||
}
|
||||
|
||||
MessageHeader header;
|
||||
std::memcpy(&header.type, source.data(), sizeof(header.type));
|
||||
std::memcpy(&header.seq, source.data() + sizeof(header.type), sizeof(header.seq));
|
||||
|
||||
return header;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
/**
|
||||
* Address a message is delivered to. Concrete values are assigned by the
|
||||
* application.
|
||||
*/
|
||||
using MessageType = uint32_t;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
/**
|
||||
* Identifies a remote peer. Assigned when the peer connects or is accepted and
|
||||
* stable until it disconnects.
|
||||
*/
|
||||
using PeerId = uint64_t;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "message_protocol/MessageConnection.hpp"
|
||||
|
||||
#include "message_protocol/MessageDispatcher.hpp"
|
||||
#include "quicr/QuicrConnection.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
namespace {
|
||||
constexpr size_t INITIAL_SEND_BUFFER_SIZE = 64 * 1024;
|
||||
}
|
||||
|
||||
MessageConnection::MessageConnection(PeerId peer_id,
|
||||
net::quicr::QuicrConnection* connection,
|
||||
MessageDispatcher* dispatcher) :
|
||||
m_connection(connection),
|
||||
m_dispatcher(dispatcher),
|
||||
m_peer_id(peer_id),
|
||||
m_send_buffer(INITIAL_SEND_BUFFER_SIZE) {
|
||||
}
|
||||
|
||||
bool MessageConnection::is_established() const {
|
||||
return m_connection->state() == net::quicr::QuicrConnectionState::Established;
|
||||
}
|
||||
|
||||
uint32_t MessageConnection::next_seq() {
|
||||
uint32_t seq = m_next_seq++;
|
||||
if(m_next_seq == MessageHeader::SEQ_NONE) {
|
||||
m_next_seq = 1;
|
||||
}
|
||||
|
||||
return seq;
|
||||
}
|
||||
|
||||
tl::expected<void, MessageError> MessageConnection::send_impl(MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
uint32_t seq,
|
||||
bool reliable) {
|
||||
const size_t size = MessageHeader::SIZE + body.size();
|
||||
|
||||
if(m_send_buffer.size() < size) {
|
||||
m_send_buffer.resize(size);
|
||||
}
|
||||
|
||||
MessageHeader{ type, seq }.encode(m_send_buffer);
|
||||
std::memcpy(m_send_buffer.data() + MessageHeader::SIZE, body.data(), body.size());
|
||||
|
||||
auto send_r = m_connection->send_message(std::span(m_send_buffer).subspan(0, size), reliable);
|
||||
if(!send_r) {
|
||||
return tl::make_unexpected(MessageError(MessageErrorType::SendFailed, send_r.error().message()));
|
||||
}
|
||||
|
||||
m_bytes_sent += size;
|
||||
m_messages_sent++;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<void, MessageError> MessageConnection::send(MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
bool reliable) {
|
||||
return send_impl(type, body, MessageHeader::SEQ_NONE, reliable);
|
||||
}
|
||||
|
||||
tl::expected<void, MessageError> MessageConnection::send_framed(std::span<const std::byte> message,
|
||||
bool reliable) {
|
||||
if(message.size() < MessageHeader::SIZE) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::SendFailed, "the message is too short to hold a header"));
|
||||
}
|
||||
|
||||
// send_message takes a writable span, so the bytes are staged in the send
|
||||
// buffer rather than sent straight from the caller's buffer.
|
||||
if(m_send_buffer.size() < message.size()) {
|
||||
m_send_buffer.resize(message.size());
|
||||
}
|
||||
|
||||
std::memcpy(m_send_buffer.data(), message.data(), message.size());
|
||||
|
||||
auto send_r = m_connection->send_message(std::span(m_send_buffer).subspan(0, message.size()), reliable);
|
||||
if(!send_r) {
|
||||
return tl::make_unexpected(MessageError(MessageErrorType::SendFailed, send_r.error().message()));
|
||||
}
|
||||
|
||||
m_bytes_sent += message.size();
|
||||
m_messages_sent++;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<void, MessageError> MessageConnection::request(MessageType type,
|
||||
std::span<const std::byte> body,
|
||||
ReplyHandler on_reply,
|
||||
std::chrono::milliseconds timeout,
|
||||
std::function<void()> on_timeout,
|
||||
bool reliable) {
|
||||
const uint32_t seq = next_seq();
|
||||
|
||||
auto send_r = send_impl(type, body, seq, reliable);
|
||||
if(!send_r) {
|
||||
return send_r;
|
||||
}
|
||||
|
||||
m_pending.emplace(seq,
|
||||
PendingRequest{ std::move(on_reply),
|
||||
std::move(on_timeout),
|
||||
std::chrono::steady_clock::now() + timeout });
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t MessageConnection::receive(std::span<std::byte> scratch) {
|
||||
size_t total = 0;
|
||||
|
||||
while(true) {
|
||||
auto read_r = m_connection->read_into(scratch);
|
||||
if(!read_r) {
|
||||
spdlog::error("Failed to read from peer {}: {}", m_peer_id, read_r.error().message());
|
||||
break;
|
||||
}
|
||||
|
||||
if(*read_r == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
total += *read_r;
|
||||
m_messages_received++;
|
||||
on_message(scratch.subspan(0, *read_r));
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
void MessageConnection::on_message(std::span<const std::byte> message) {
|
||||
auto header = MessageHeader::decode(message);
|
||||
if(!header) {
|
||||
spdlog::warn("Dropped a message of {} bytes, too short to hold a header", message.size());
|
||||
return;
|
||||
}
|
||||
|
||||
auto body = message.subspan(MessageHeader::SIZE);
|
||||
|
||||
if(header->seq != MessageHeader::SEQ_NONE) {
|
||||
auto pending = m_pending.find(header->seq);
|
||||
if(pending != m_pending.end()) {
|
||||
auto on_reply = std::move(pending->second.on_reply);
|
||||
m_pending.erase(pending);
|
||||
on_reply(body);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!m_dispatcher->dispatch(m_peer_id, header->type, body)) {
|
||||
spdlog::warn("No handler for message type {}", header->type);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageConnection::expire_requests(std::chrono::steady_clock::time_point now) {
|
||||
std::erase_if(m_pending, [&](auto& entry) {
|
||||
if(entry.second.expires_at > now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spdlog::warn("Request {} timed out", entry.first);
|
||||
if(entry.second.on_timeout) {
|
||||
entry.second.on_timeout();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
|
||||
#include "quicr/QuicrAddress.hpp"
|
||||
#include "quicr/QuicrConnection.hpp"
|
||||
#include "quicr/QuicrConnectionListener.hpp"
|
||||
#include "quicr/QuicrEndpoint.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace tw::msg {
|
||||
|
||||
namespace {
|
||||
constexpr size_t RECEIVE_BUFFER_SIZE = 64 * 1024;
|
||||
}
|
||||
|
||||
MessageEndpoint::MessageEndpoint(std::unique_ptr<net::quicr::QuicrEndpoint> endpoint) :
|
||||
m_endpoint(std::move(endpoint)),
|
||||
m_receive_buffer(RECEIVE_BUFFER_SIZE) {
|
||||
}
|
||||
|
||||
MessageEndpoint::~MessageEndpoint() = default;
|
||||
|
||||
tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> MessageEndpoint::create() {
|
||||
auto endpoint_r = net::quicr::QuicrEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::BindFailed, endpoint_r.error().message()));
|
||||
}
|
||||
|
||||
return std::unique_ptr<MessageEndpoint>(new MessageEndpoint(std::move(endpoint_r.value())));
|
||||
}
|
||||
|
||||
tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> MessageEndpoint::bind(int port) {
|
||||
auto endpoint_r = create();
|
||||
if(!endpoint_r) {
|
||||
return endpoint_r;
|
||||
}
|
||||
|
||||
auto& endpoint = endpoint_r.value();
|
||||
|
||||
auto bind_r = endpoint->m_endpoint->bind(port);
|
||||
if(!bind_r) {
|
||||
return tl::make_unexpected(MessageError(MessageErrorType::BindFailed, bind_r.error().message()));
|
||||
}
|
||||
|
||||
auto listener_r = net::quicr::QuicrConnectionListener::listen(endpoint->m_endpoint.get());
|
||||
if(!listener_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::BindFailed, listener_r.error().message()));
|
||||
}
|
||||
|
||||
endpoint->m_listener = std::move(listener_r.value());
|
||||
|
||||
return endpoint_r;
|
||||
}
|
||||
|
||||
MessageConnection* MessageEndpoint::add_peer(net::quicr::QuicrConnection* connection) {
|
||||
const PeerId id = m_next_peer_id++;
|
||||
|
||||
auto peer = std::make_unique<MessageConnection>(id, connection, &m_dispatcher);
|
||||
auto* raw = peer.get();
|
||||
|
||||
m_peers.emplace(id, std::move(peer));
|
||||
|
||||
if(m_on_peer_connected) {
|
||||
m_on_peer_connected(id);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
tl::expected<MessageConnection*, MessageError> MessageEndpoint::connect(const std::string& host, int port) {
|
||||
auto connection_r = m_endpoint->connect(net::quicr::QuicrAddress(host, port));
|
||||
if(!connection_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::ConnectFailed, connection_r.error().message()));
|
||||
}
|
||||
|
||||
return add_peer(connection_r.value());
|
||||
}
|
||||
|
||||
void MessageEndpoint::accept_peers() {
|
||||
if(!m_listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
while(net::quicr::QuicrConnection* connection = m_listener->listen()) {
|
||||
add_peer(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEndpoint::receive() {
|
||||
for(auto& [id, peer] : m_peers) {
|
||||
m_bytes_received += peer->receive(m_receive_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEndpoint::update() {
|
||||
m_endpoint->poll();
|
||||
|
||||
accept_peers();
|
||||
receive();
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
for(auto& [id, peer] : m_peers) {
|
||||
peer->expire_requests(now);
|
||||
}
|
||||
}
|
||||
|
||||
MessageConnection* MessageEndpoint::peer(PeerId id) {
|
||||
auto peer = m_peers.find(id);
|
||||
return peer != m_peers.end() ? peer->second.get() : nullptr;
|
||||
}
|
||||
|
||||
std::vector<MessageConnection*> MessageEndpoint::peers() const {
|
||||
std::vector<MessageConnection*> result;
|
||||
result.reserve(m_peers.size());
|
||||
|
||||
for(const auto& [id, peer] : m_peers) {
|
||||
result.push_back(peer.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MessageEndpoint::broadcast(MessageType type, std::span<const std::byte> body, bool reliable) {
|
||||
for(auto& [id, peer] : m_peers) {
|
||||
auto send_r = peer->send(type, body, reliable);
|
||||
if(!send_r) {
|
||||
spdlog::error("Failed to send to peer {}: {}", id, send_r.error().message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MessageEndpoint::bytes_sent() const {
|
||||
uint64_t total = 0;
|
||||
for(const auto& [id, peer] : m_peers) {
|
||||
total += peer->bytes_sent();
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t MessageEndpoint::messages_sent() const {
|
||||
uint64_t total = 0;
|
||||
for(const auto& [id, peer] : m_peers) {
|
||||
total += peer->messages_sent();
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t MessageEndpoint::messages_received() const {
|
||||
uint64_t total = 0;
|
||||
for(const auto& [id, peer] : m_peers) {
|
||||
total += peer->messages_received();
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
project(tw_message_protocol_tests)
|
||||
|
||||
file(GLOB FILES
|
||||
./*.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${FILES})
|
||||
|
||||
# tw::quicr is listed explicitly because CMake does not propagate the object
|
||||
# files of an OBJECT library through another OBJECT library.
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
tw::message_protocol
|
||||
tw::quicr
|
||||
Catch2::Catch2WithMain
|
||||
tl::expected
|
||||
)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
|
||||
catch_discover_tests(${PROJECT_NAME})
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "message_protocol/MessageDispatcher.hpp"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using namespace tw::msg;
|
||||
|
||||
namespace {
|
||||
|
||||
std::span<const std::byte> as_bytes(const std::array<std::byte, 2>& body) {
|
||||
return { body.data(), body.size() };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("Dispatch reaches the handler bound to the address", "[message_dispatcher]") {
|
||||
MessageDispatcher dispatcher;
|
||||
|
||||
PeerId seen_peer = 0;
|
||||
size_t seen_size = 0;
|
||||
|
||||
dispatcher.set_handler(7, [&](PeerId peer, std::span<const std::byte> body) {
|
||||
seen_peer = peer;
|
||||
seen_size = body.size();
|
||||
});
|
||||
|
||||
std::array<std::byte, 2> body{};
|
||||
|
||||
REQUIRE(dispatcher.dispatch(99, 7, as_bytes(body)));
|
||||
REQUIRE(seen_peer == 99);
|
||||
REQUIRE(seen_size == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Dispatch to an unbound address reports failure", "[message_dispatcher]") {
|
||||
MessageDispatcher dispatcher;
|
||||
|
||||
std::array<std::byte, 2> body{};
|
||||
|
||||
REQUIRE_FALSE(dispatcher.dispatch(1, 7, as_bytes(body)));
|
||||
}
|
||||
|
||||
TEST_CASE("Addresses are routed independently", "[message_dispatcher]") {
|
||||
MessageDispatcher dispatcher;
|
||||
|
||||
std::string called;
|
||||
|
||||
dispatcher.set_handler(1, [&](PeerId, std::span<const std::byte>) { called = "first"; });
|
||||
dispatcher.set_handler(2, [&](PeerId, std::span<const std::byte>) { called = "second"; });
|
||||
|
||||
std::array<std::byte, 2> body{};
|
||||
|
||||
dispatcher.dispatch(1, 2, as_bytes(body));
|
||||
REQUIRE(called == "second");
|
||||
|
||||
dispatcher.dispatch(1, 1, as_bytes(body));
|
||||
REQUIRE(called == "first");
|
||||
}
|
||||
|
||||
TEST_CASE("Rebinding an address replaces the handler", "[message_dispatcher]") {
|
||||
MessageDispatcher dispatcher;
|
||||
|
||||
std::string called;
|
||||
|
||||
dispatcher.set_handler(7, [&](PeerId, std::span<const std::byte>) { called = "first"; });
|
||||
dispatcher.set_handler(7, [&](PeerId, std::span<const std::byte>) { called = "second"; });
|
||||
|
||||
std::array<std::byte, 2> body{};
|
||||
dispatcher.dispatch(1, 7, as_bytes(body));
|
||||
|
||||
REQUIRE(called == "second");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "message_protocol/MessageHeader.hpp"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
using namespace tw::msg;
|
||||
|
||||
TEST_CASE("Header survives a round trip", "[message_header]") {
|
||||
std::array<std::byte, MessageHeader::SIZE> buffer{};
|
||||
|
||||
MessageHeader{ 42, 7 }.encode(buffer);
|
||||
|
||||
auto decoded = MessageHeader::decode(buffer);
|
||||
|
||||
REQUIRE(decoded.has_value());
|
||||
REQUIRE(decoded->type == 42);
|
||||
REQUIRE(decoded->seq == 7);
|
||||
}
|
||||
|
||||
TEST_CASE("Header defaults to expecting no reply", "[message_header]") {
|
||||
std::array<std::byte, MessageHeader::SIZE> buffer{};
|
||||
|
||||
MessageHeader{ 3 }.encode(buffer);
|
||||
|
||||
auto decoded = MessageHeader::decode(buffer);
|
||||
|
||||
REQUIRE(decoded.has_value());
|
||||
REQUIRE(decoded->seq == MessageHeader::SEQ_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("Decoding a message shorter than a header fails", "[message_header]") {
|
||||
std::array<std::byte, MessageHeader::SIZE - 1> buffer{};
|
||||
|
||||
REQUIRE_FALSE(MessageHeader::decode(buffer).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("Encoding only writes the header", "[message_header]") {
|
||||
std::array<std::byte, MessageHeader::SIZE + 4> buffer{};
|
||||
buffer[MessageHeader::SIZE] = std::byte{ 0xAB };
|
||||
|
||||
MessageHeader{ 1, 2 }.encode(buffer);
|
||||
|
||||
REQUIRE(buffer[MessageHeader::SIZE] == std::byte{ 0xAB });
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
project(tw_messaging)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
add_library(tw::messaging ALIAS ${PROJECT_NAME})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE ./include/)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
tw::network
|
||||
tw::protocol
|
||||
tl::expected
|
||||
protobuf::libprotobuf
|
||||
)
|
||||
@@ -1,158 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "protocol/quicr/QuicrEndpoint.hpp"
|
||||
#include "protocol/quicr/QuicrConnection.hpp"
|
||||
#include "protocol/quicr/QuicrError.hpp"
|
||||
|
||||
#include <tl/expected.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace tw {
|
||||
|
||||
// Bidirectional typed messaging layer. Owns its QuicrEndpoint and connection.
|
||||
// Wire format: [uint32_t type][uint32_t seq][payload bytes]
|
||||
// seq == 0 means fire-and-forget; non-zero seq correlates a reply to a request().
|
||||
// Protocol-agnostic: callers are responsible for serialising/deserialising payloads.
|
||||
// update() polls the endpoint and dispatches inbound data automatically.
|
||||
class MessageSession {
|
||||
static constexpr size_t MAX_TYPES = 32;
|
||||
static constexpr uint32_t SEQ_NONE = 0;
|
||||
|
||||
struct PendingRequest {
|
||||
std::function<void(std::span<const std::byte>)> handler;
|
||||
std::chrono::steady_clock::time_point expires_at;
|
||||
std::function<void()> on_timeout;
|
||||
};
|
||||
|
||||
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
|
||||
net::quicr::QuicrConnection* m_conn;
|
||||
std::vector<std::byte> m_recv_buf{64 * 1024};
|
||||
std::vector<std::function<void(std::span<const std::byte>)>> m_handlers{MAX_TYPES};
|
||||
std::unordered_map<uint32_t, PendingRequest> m_pending{};
|
||||
uint32_t m_next_seq = 1;
|
||||
|
||||
public:
|
||||
// Creates a QuicrEndpoint, connects to the given address, and owns both.
|
||||
// Throws on failure (via tl::expected::value()).
|
||||
explicit MessageSession(net::Address address)
|
||||
: m_endpoint(std::make_unique<net::quicr::QuicrEndpoint>(net::quicr::QuicrEndpoint::create().value()))
|
||||
, m_conn(m_endpoint->connect(address).value())
|
||||
{}
|
||||
|
||||
MessageSession(const MessageSession&) = delete;
|
||||
MessageSession& operator=(const MessageSession&) = delete;
|
||||
MessageSession(MessageSession&&) = default;
|
||||
MessageSession& operator=(MessageSession&&) = default;
|
||||
|
||||
// Register a permanent handler for the given type ID.
|
||||
void set_handler(uint32_t type, std::function<void(std::span<const std::byte>)> fn) {
|
||||
if (type >= m_handlers.size()) {
|
||||
spdlog::warn("MessageSession: type {} exceeds MAX_TYPES", type);
|
||||
return;
|
||||
}
|
||||
m_handlers[type] = std::move(fn);
|
||||
}
|
||||
|
||||
// Send a request with a one-shot response handler matched by sequence number.
|
||||
// Call update() each game tick to evict timed-out requests.
|
||||
tl::expected<void, net::quicr::QuicrError> request(
|
||||
uint32_t type,
|
||||
std::span<const std::byte> payload,
|
||||
std::function<void(std::span<const std::byte>)> on_response,
|
||||
std::chrono::milliseconds timeout = std::chrono::seconds(5),
|
||||
std::function<void()> on_timeout = nullptr,
|
||||
bool reliable = true
|
||||
) {
|
||||
const uint32_t seq = m_next_seq++;
|
||||
if (m_next_seq == SEQ_NONE) m_next_seq = 1;
|
||||
|
||||
m_pending.emplace(seq, PendingRequest{
|
||||
std::move(on_response),
|
||||
std::chrono::steady_clock::now() + timeout,
|
||||
std::move(on_timeout)
|
||||
});
|
||||
return send_impl(type, payload, seq, reliable);
|
||||
}
|
||||
|
||||
// Expire timed-out pending requests, poll the endpoint, and dispatch any
|
||||
// inbound datagrams. Call once per game tick.
|
||||
void update() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::erase_if(m_pending, [&](auto& kv) {
|
||||
if (kv.second.expires_at > now) return false;
|
||||
spdlog::warn("MessageSession: request seq={} timed out", kv.first);
|
||||
if (kv.second.on_timeout) kv.second.on_timeout();
|
||||
return true;
|
||||
});
|
||||
|
||||
m_endpoint->poll();
|
||||
while (true) {
|
||||
auto r = m_conn->read_into(m_recv_buf);
|
||||
if (!r || *r == 0) break;
|
||||
dispatch(std::span(m_recv_buf.data(), *r));
|
||||
}
|
||||
}
|
||||
|
||||
// Decode one framed datagram: [uint32_t type][uint32_t seq][payload].
|
||||
// Non-zero seq routes to a pending one-shot handler; seq==0 routes by type.
|
||||
void dispatch(std::span<const std::byte> data) {
|
||||
constexpr size_t HEADER = sizeof(uint32_t) * 2;
|
||||
if (data.size() < HEADER) {
|
||||
spdlog::warn("MessageSession: dropped short datagram ({} bytes)", data.size());
|
||||
return;
|
||||
}
|
||||
uint32_t type{}, seq{};
|
||||
std::memcpy(&type, data.data(), sizeof(type));
|
||||
std::memcpy(&seq, data.data() + sizeof(uint32_t), sizeof(seq));
|
||||
|
||||
const auto payload = data.subspan(HEADER);
|
||||
|
||||
if (seq != SEQ_NONE) {
|
||||
if (auto it = m_pending.find(seq); it != m_pending.end()) {
|
||||
auto handler = std::move(it->second.handler);
|
||||
m_pending.erase(it);
|
||||
handler(payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (type >= m_handlers.size() || !m_handlers[type]) {
|
||||
spdlog::warn("MessageSession: no handler for type {}", type);
|
||||
return;
|
||||
}
|
||||
m_handlers[type](payload);
|
||||
}
|
||||
|
||||
// Send a fire-and-forget message.
|
||||
tl::expected<void, net::quicr::QuicrError> send(uint32_t type,
|
||||
std::span<const std::byte> payload,
|
||||
bool reliable = false) {
|
||||
return send_impl(type, payload, SEQ_NONE, reliable);
|
||||
}
|
||||
|
||||
bool is_established() const {
|
||||
return m_conn->state() == net::quicr::QuicrConnectionState::Established;
|
||||
}
|
||||
|
||||
private:
|
||||
tl::expected<void, net::quicr::QuicrError> send_impl(uint32_t type,
|
||||
std::span<const std::byte> payload,
|
||||
uint32_t seq,
|
||||
bool reliable) {
|
||||
std::vector<std::byte> buf(sizeof(type) + sizeof(seq) + payload.size());
|
||||
std::memcpy(buf.data(), &type, sizeof(type));
|
||||
std::memcpy(buf.data() + sizeof(type), &seq, sizeof(seq));
|
||||
std::memcpy(buf.data() + sizeof(type) + sizeof(seq), payload.data(), payload.size());
|
||||
return m_conn->send_message(std::span(buf), reliable);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw
|
||||
@@ -0,0 +1,19 @@
|
||||
project(tw_metrics)
|
||||
|
||||
add_subdirectory(tests)
|
||||
|
||||
file(GLOB HEADERS
|
||||
include/metrics/*.hpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
add_library(tw::metrics ALIAS ${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME}
|
||||
INTERFACE FILE_SET HEADERS
|
||||
BASE_DIRS include
|
||||
FILES ${HEADERS})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
${PROJECT_SOURCE_DIR}/include/
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::metrics {
|
||||
|
||||
/**
|
||||
* Aggregate of every value that fell into one bucket.
|
||||
*
|
||||
* Answers sum, average, minimum and maximum without keeping the individual
|
||||
* values. A sample nothing was added to reports zero for all of them, so gaps
|
||||
* read as zero rather than as an unset extreme.
|
||||
*/
|
||||
struct MetricSample {
|
||||
uint32_t count = 0;
|
||||
double sum = 0.0;
|
||||
double min = 0.0;
|
||||
double max = 0.0;
|
||||
|
||||
void add(double value) {
|
||||
if(count == 0) {
|
||||
min = value;
|
||||
max = value;
|
||||
} else {
|
||||
min = std::min(min, value);
|
||||
max = std::max(max, value);
|
||||
}
|
||||
|
||||
sum += value;
|
||||
count++;
|
||||
}
|
||||
|
||||
/** Folds `other` in, as if its values had been added to this sample. */
|
||||
void merge(const MetricSample& other) {
|
||||
if(other.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(count == 0) {
|
||||
*this = other;
|
||||
return;
|
||||
}
|
||||
|
||||
min = std::min(min, other.min);
|
||||
max = std::max(max, other.max);
|
||||
|
||||
sum += other.sum;
|
||||
count += other.count;
|
||||
}
|
||||
|
||||
double avg() const {
|
||||
return count == 0 ? 0.0 : sum / count;
|
||||
}
|
||||
|
||||
bool is_empty() const {
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
*this = {};
|
||||
}
|
||||
};
|
||||
|
||||
/** Statistic to read out of a sample. */
|
||||
enum class MetricField {
|
||||
Avg,
|
||||
Min,
|
||||
Max,
|
||||
Sum,
|
||||
Count
|
||||
};
|
||||
|
||||
inline double value_of(const MetricSample& sample, MetricField field) {
|
||||
switch(field) {
|
||||
case MetricField::Avg: return sample.avg();
|
||||
case MetricField::Min: return sample.min;
|
||||
case MetricField::Max: return sample.max;
|
||||
case MetricField::Sum: return sample.sum;
|
||||
case MetricField::Count: return (double)sample.count;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/MetricSample.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::metrics {
|
||||
|
||||
/**
|
||||
* Ring of samples, one bucket per `Interval` of elapsed time.
|
||||
*
|
||||
* Values pushed during the same interval fold into one bucket, and intervals
|
||||
* that pass without a value become empty buckets, so the distance between two
|
||||
* buckets always matches the time between them. Once `capacity` buckets are
|
||||
* held the oldest one is dropped.
|
||||
*/
|
||||
template<typename Interval, typename Clock = std::chrono::steady_clock>
|
||||
class MetricSeries {
|
||||
public:
|
||||
using TimePoint = typename Clock::time_point;
|
||||
|
||||
private:
|
||||
std::vector<MetricSample> m_buckets;
|
||||
|
||||
/** Index of the newest bucket, in `Interval` units since the clock epoch. */
|
||||
int64_t m_newest = 0;
|
||||
|
||||
/** Buckets holding data, counted back from the newest. */
|
||||
size_t m_count = 0;
|
||||
|
||||
static int64_t bucket_of(TimePoint time) {
|
||||
return (int64_t)std::chrono::floor<Interval>(time).time_since_epoch().count();
|
||||
}
|
||||
|
||||
size_t slot_of(int64_t index) const {
|
||||
int64_t size = (int64_t)m_buckets.size();
|
||||
int64_t slot = index % size;
|
||||
|
||||
return (size_t)(slot < 0 ? slot + size : slot);
|
||||
}
|
||||
|
||||
MetricSample& bucket_at(int64_t index) {
|
||||
return m_buckets[slot_of(index)];
|
||||
}
|
||||
|
||||
const MetricSample& bucket_at(int64_t index) const {
|
||||
return m_buckets[slot_of(index)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the newest bucket up to `index`, emptying every bucket the gap
|
||||
* covers. A gap wider than the ring empties all of it.
|
||||
*/
|
||||
void advance_to(int64_t index) {
|
||||
int64_t steps = index - m_newest;
|
||||
int64_t capacity = (int64_t)m_buckets.size();
|
||||
int64_t to_clear = std::min(steps, capacity);
|
||||
|
||||
for(int64_t i = 0; i < to_clear; i++) {
|
||||
bucket_at(index - i).reset();
|
||||
}
|
||||
|
||||
m_newest = index;
|
||||
m_count = steps >= capacity
|
||||
? m_buckets.size()
|
||||
: std::min(m_count + (size_t)steps, m_buckets.size());
|
||||
}
|
||||
|
||||
public:
|
||||
explicit MetricSeries(size_t capacity) :
|
||||
m_buckets(capacity)
|
||||
{
|
||||
if(capacity == 0) {
|
||||
throw std::invalid_argument("`capacity` must hold at least one bucket");
|
||||
}
|
||||
}
|
||||
|
||||
size_t capacity() const {
|
||||
return m_buckets.size();
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return m_count;
|
||||
}
|
||||
|
||||
bool is_empty() const {
|
||||
return m_count == 0;
|
||||
}
|
||||
|
||||
/** Index of the newest bucket, in `Interval` units since the clock epoch. */
|
||||
int64_t newest_index() const {
|
||||
return m_newest;
|
||||
}
|
||||
|
||||
void push(double value) {
|
||||
push(value, Clock::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `value` to the bucket `at` falls into. A value older than every
|
||||
* bucket still held is dropped.
|
||||
*/
|
||||
void push(double value, TimePoint at) {
|
||||
int64_t index = bucket_of(at);
|
||||
|
||||
if(m_count == 0) {
|
||||
m_newest = index;
|
||||
m_count = 1;
|
||||
|
||||
bucket_at(index).reset();
|
||||
bucket_at(index).add(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if(index > m_newest) {
|
||||
advance_to(index);
|
||||
} else if(m_newest - index >= (int64_t)m_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket_at(index).add(value);
|
||||
}
|
||||
|
||||
/** Newest first: age 0 is the bucket currently being filled. */
|
||||
const MetricSample& at_age(size_t age) const {
|
||||
return bucket_at(m_newest - (int64_t)age);
|
||||
}
|
||||
|
||||
/** Aggregate of the newest `buckets` buckets. */
|
||||
MetricSample window(size_t buckets) const {
|
||||
MetricSample result;
|
||||
|
||||
size_t count = std::min(buckets, m_count);
|
||||
for(size_t age = 0; age < count; age++) {
|
||||
result.merge(at_age(age));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Aggregate of everything still held. */
|
||||
MetricSample window() const {
|
||||
return window(m_count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the newest `buckets` buckets into `xs` and `ys` oldest first, as
|
||||
* two contiguous arrays. `xs` holds the age of each bucket in `Interval`
|
||||
* units, so the bucket being filled sits at 0 and older ones run negative.
|
||||
* Both vectors are resized to the number of points written.
|
||||
*
|
||||
* `skip_newest` leaves that many of the newest buckets out. The bucket
|
||||
* being filled only holds the part of its interval that has elapsed, so
|
||||
* reading it next to whole ones makes the newest point dip and recover;
|
||||
* skipping it keeps every point covering the same span of time. Ages stay
|
||||
* true, so a skipped bucket leaves a gap rather than shifting the rest.
|
||||
*/
|
||||
size_t linearize(std::vector<double>& xs,
|
||||
std::vector<double>& ys,
|
||||
MetricField field,
|
||||
size_t buckets,
|
||||
size_t skip_newest = 0) const {
|
||||
size_t available = m_count > skip_newest ? m_count - skip_newest : 0;
|
||||
size_t count = std::min(buckets, available);
|
||||
|
||||
xs.resize(count);
|
||||
ys.resize(count);
|
||||
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
size_t age = skip_newest + count - 1 - i;
|
||||
|
||||
xs[i] = -(double)age;
|
||||
ys[i] = value_of(at_age(age), field);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t linearize(std::vector<double>& xs,
|
||||
std::vector<double>& ys,
|
||||
MetricField field) const {
|
||||
return linearize(xs, ys, field, m_count);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
m_count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
project(tw_metrics_tests)
|
||||
|
||||
set(LIBS
|
||||
tw::metrics
|
||||
)
|
||||
|
||||
file(GLOB FILES
|
||||
./*.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${FILES})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${LIBS}
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
|
||||
catch_discover_tests(${PROJECT_NAME})
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "metrics/MetricSample.hpp"
|
||||
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
using tw::metrics::MetricField;
|
||||
using tw::metrics::MetricSample;
|
||||
using tw::metrics::value_of;
|
||||
|
||||
TEST_CASE("Empty sample reports zero", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
REQUIRE(sample.is_empty());
|
||||
REQUIRE(sample.count == 0);
|
||||
REQUIRE(sample.sum == 0.0);
|
||||
REQUIRE(sample.min == 0.0);
|
||||
REQUIRE(sample.max == 0.0);
|
||||
REQUIRE(sample.avg() == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Sample tracks sum, average and extremes", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
sample.add(4.0);
|
||||
sample.add(1.0);
|
||||
sample.add(7.0);
|
||||
|
||||
REQUIRE(sample.count == 3);
|
||||
REQUIRE(sample.sum == 12.0);
|
||||
REQUIRE(sample.min == 1.0);
|
||||
REQUIRE(sample.max == 7.0);
|
||||
REQUIRE(sample.avg() == 4.0);
|
||||
}
|
||||
|
||||
TEST_CASE("First value sets both extremes", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
sample.add(-5.0);
|
||||
|
||||
REQUIRE(sample.min == -5.0);
|
||||
REQUIRE(sample.max == -5.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merge folds one sample into another", "[metric_sample]") {
|
||||
MetricSample left;
|
||||
left.add(2.0);
|
||||
left.add(4.0);
|
||||
|
||||
MetricSample right;
|
||||
right.add(10.0);
|
||||
right.add(0.5);
|
||||
|
||||
left.merge(right);
|
||||
|
||||
REQUIRE(left.count == 4);
|
||||
REQUIRE(left.sum == 16.5);
|
||||
REQUIRE(left.min == 0.5);
|
||||
REQUIRE(left.max == 10.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merging with an empty sample changes nothing", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(3.0);
|
||||
|
||||
sample.merge(MetricSample{});
|
||||
|
||||
REQUIRE(sample.count == 1);
|
||||
REQUIRE(sample.min == 3.0);
|
||||
REQUIRE(sample.max == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merging into an empty sample adopts the other", "[metric_sample]") {
|
||||
MetricSample other;
|
||||
other.add(3.0);
|
||||
other.add(9.0);
|
||||
|
||||
MetricSample sample;
|
||||
sample.merge(other);
|
||||
|
||||
REQUIRE(sample.count == 2);
|
||||
REQUIRE(sample.sum == 12.0);
|
||||
REQUIRE(sample.min == 3.0);
|
||||
REQUIRE(sample.max == 9.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Field selects the statistic to read", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(2.0);
|
||||
sample.add(6.0);
|
||||
|
||||
REQUIRE(value_of(sample, MetricField::Avg) == 4.0);
|
||||
REQUIRE(value_of(sample, MetricField::Min) == 2.0);
|
||||
REQUIRE(value_of(sample, MetricField::Max) == 6.0);
|
||||
REQUIRE(value_of(sample, MetricField::Sum) == 8.0);
|
||||
REQUIRE(value_of(sample, MetricField::Count) == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Reset empties the sample", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(5.0);
|
||||
|
||||
sample.reset();
|
||||
|
||||
REQUIRE(sample.is_empty());
|
||||
REQUIRE(sample.max == 0.0);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using tw::metrics::MetricField;
|
||||
using tw::metrics::MetricSeries;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Series = MetricSeries<std::chrono::seconds>;
|
||||
|
||||
/** Fixed origin so every test drives the series by hand. */
|
||||
static Clock::time_point at(int64_t seconds) {
|
||||
return Clock::time_point{} + std::chrono::hours(1) + std::chrono::seconds(seconds);
|
||||
}
|
||||
|
||||
TEST_CASE("Series starts empty", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
REQUIRE(series.capacity() == 8);
|
||||
REQUIRE(series.size() == 0);
|
||||
REQUIRE(series.is_empty());
|
||||
REQUIRE(series.window().is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Series rejects a zero capacity", "[metric_series]") {
|
||||
REQUIRE_THROWS_AS(Series(0), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE("Values in the same interval fold into one bucket", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(3.0, at(0));
|
||||
|
||||
REQUIRE(series.size() == 1);
|
||||
REQUIRE(series.at_age(0).count == 2);
|
||||
REQUIRE(series.at_age(0).avg() == 2.0);
|
||||
REQUIRE(series.at_age(0).min == 1.0);
|
||||
REQUIRE(series.at_age(0).max == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Values in different intervals land in different buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(5.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 2);
|
||||
REQUIRE(series.at_age(0).sum == 5.0);
|
||||
REQUIRE(series.at_age(1).sum == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Intervals without a value become empty buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(4.0, at(3));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 4.0);
|
||||
REQUIRE(series.at_age(1).is_empty());
|
||||
REQUIRE(series.at_age(2).is_empty());
|
||||
REQUIRE(series.at_age(3).sum == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Series never holds more than its capacity", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
for(int64_t i = 0; i < 10; i++) {
|
||||
series.push((double)i, at(i));
|
||||
}
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 9.0);
|
||||
REQUIRE(series.at_age(3).sum == 6.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A gap wider than the ring leaves only the newest bucket filled", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(100));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
REQUIRE(series.at_age(1).is_empty());
|
||||
REQUIRE(series.at_age(2).is_empty());
|
||||
REQUIRE(series.at_age(3).is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Buckets dropped by wrapping do not come back", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
series.push(100.0, at(0));
|
||||
|
||||
for(int64_t i = 1; i < 5; i++) {
|
||||
series.push(1.0, at(i));
|
||||
}
|
||||
|
||||
REQUIRE(series.window().max == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Window aggregates across buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(4.0, at(0));
|
||||
series.push(1.0, at(1));
|
||||
series.push(7.0, at(2));
|
||||
|
||||
auto window = series.window();
|
||||
|
||||
REQUIRE(window.count == 3);
|
||||
REQUIRE(window.sum == 12.0);
|
||||
REQUIRE(window.min == 1.0);
|
||||
REQUIRE(window.max == 7.0);
|
||||
REQUIRE(window.avg() == 4.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Window can be narrowed to the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(4.0, at(0));
|
||||
series.push(1.0, at(1));
|
||||
series.push(7.0, at(2));
|
||||
|
||||
auto window = series.window(2);
|
||||
|
||||
REQUIRE(window.count == 2);
|
||||
REQUIRE(window.min == 1.0);
|
||||
REQUIRE(window.max == 7.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Empty buckets do not skew the window extremes", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(5.0, at(0));
|
||||
series.push(9.0, at(4));
|
||||
|
||||
auto window = series.window();
|
||||
|
||||
REQUIRE(window.count == 2);
|
||||
REQUIRE(window.min == 5.0);
|
||||
REQUIRE(window.max == 9.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A late value folds into the bucket it belongs to", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(2));
|
||||
series.push(6.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 3);
|
||||
REQUIRE(series.at_age(1).sum == 6.0);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A value older than every bucket held is dropped", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
for(int64_t i = 0; i < 4; i++) {
|
||||
series.push(1.0, at(i));
|
||||
}
|
||||
|
||||
series.push(99.0, at(-10));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.window().max == 1.0);
|
||||
REQUIRE(series.window().count == 4);
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize writes buckets oldest first", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum);
|
||||
|
||||
REQUIRE(count == 3);
|
||||
REQUIRE(xs == std::vector<double>{-2.0, -1.0, 0.0});
|
||||
REQUIRE(ys == std::vector<double>{1.0, 2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize can be limited to the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 2);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(xs == std::vector<double>{-1.0, 0.0});
|
||||
REQUIRE(ys == std::vector<double>{2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize can leave out the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 8, 1);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(ys == std::vector<double>{1.0, 2.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Skipping the newest bucket keeps the ages of the rest", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
series.linearize(xs, ys, MetricField::Sum, 8, 1);
|
||||
|
||||
REQUIRE(xs == std::vector<double>{-2.0, -1.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Skipping more buckets than are held writes nothing", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 8, 4);
|
||||
|
||||
REQUIRE(count == 0);
|
||||
REQUIRE(xs.empty());
|
||||
REQUIRE(ys.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("A limit counts buckets that were not skipped", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
for(int64_t i = 0; i < 5; i++) {
|
||||
series.push((double)i, at(i));
|
||||
}
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 2, 1);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(ys == std::vector<double>{2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize reports empty buckets as zero", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(5.0, at(0));
|
||||
series.push(9.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
series.linearize(xs, ys, MetricField::Max);
|
||||
|
||||
REQUIRE(ys == std::vector<double>{5.0, 0.0, 9.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize resizes the vectors it is given", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
|
||||
std::vector<double> xs(64, 7.0);
|
||||
std::vector<double> ys(64, 7.0);
|
||||
|
||||
series.linearize(xs, ys, MetricField::Avg);
|
||||
|
||||
REQUIRE(xs.size() == 1);
|
||||
REQUIRE(ys.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Clear empties the series but keeps its capacity", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.clear();
|
||||
|
||||
REQUIRE(series.is_empty());
|
||||
REQUIRE(series.capacity() == 8);
|
||||
|
||||
series.push(2.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 1);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A coarser interval folds more values together", "[metric_series]") {
|
||||
MetricSeries<std::chrono::minutes> series(4);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(30));
|
||||
series.push(3.0, at(90));
|
||||
|
||||
REQUIRE(series.size() == 2);
|
||||
REQUIRE(series.at_age(1).count == 2);
|
||||
REQUIRE(series.at_age(0).sum == 3.0);
|
||||
}
|
||||
@@ -10,7 +10,9 @@ target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
towards
|
||||
tw::network
|
||||
tw::quicr
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
glm::glm
|
||||
EnTT::EnTT
|
||||
Jolt
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
#include "Address.hpp"
|
||||
#include "Login.pb.h"
|
||||
#include "PlayerMove.pb.h"
|
||||
#include "TcpStream.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "WorldState.pb.h"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "messenger/Messenger.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <semaphore>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
tw::net::MessageHandler create_messenger(tw::net::Address& address) {
|
||||
return tw::net::MessageHandler(address);
|
||||
static std::unique_ptr<tw::msg::MessageEndpoint> create_endpoint() {
|
||||
auto endpoint_r = tw::msg::MessageEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
throw std::runtime_error("Failed to create the endpoint: " + endpoint_r.error().message());
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
static tw::msg::MessageConnection* connect_to_server(tw::msg::MessageEndpoint* endpoint,
|
||||
tw::net::Address address) {
|
||||
auto server_r = endpoint->connect(address.ip_string(), address.port());
|
||||
if(!server_r) {
|
||||
throw std::runtime_error("Failed to connect to server: " + server_r.error().message());
|
||||
}
|
||||
|
||||
return server_r.value();
|
||||
}
|
||||
|
||||
class MockClient {
|
||||
tw::net::MessageHandler m_handler;
|
||||
std::unique_ptr<tw::msg::MessageEndpoint> m_endpoint;
|
||||
tw::msg::MessageConnection* m_server;
|
||||
tw::ProtobufMessages m_messages;
|
||||
|
||||
tw::LockStep m_lock_step;
|
||||
|
||||
uint32_t m_frame_idx;
|
||||
@@ -33,28 +52,30 @@ class MockClient {
|
||||
|
||||
public:
|
||||
MockClient(tw::net::Address address, const std::string& name) :
|
||||
m_handler(create_messenger(address)),
|
||||
m_endpoint(create_endpoint()),
|
||||
m_server(connect_to_server(m_endpoint.get(), address)),
|
||||
m_messages(m_endpoint.get()),
|
||||
m_lock_step(20),
|
||||
m_is_running(true),
|
||||
m_is_connected(false),
|
||||
m_connected_semaphore(0)
|
||||
{
|
||||
m_handler.set_handler<mmo::WorldStateMessage>(
|
||||
[&](mmo::WorldStateMessage* mesg) { });
|
||||
m_messages.set_handler<mmo::WorldStateMessage>(
|
||||
[this](tw::msg::PeerId, const mmo::WorldStateMessage& mesg) { });
|
||||
|
||||
m_handler.set_handler<mmo::LoginResponse>(
|
||||
[&](mmo::LoginResponse* mesg) {
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](tw::msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
if(!m_is_connected) {
|
||||
m_is_connected = true;
|
||||
m_entity_id = mesg->entity_id();
|
||||
m_entity_id = mesg.entity_id();
|
||||
m_connected_semaphore.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void run() {
|
||||
while(!m_handler.is_connected()) {
|
||||
m_handler.update();
|
||||
while(!m_server->is_established()) {
|
||||
m_endpoint->update();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
@@ -67,7 +88,7 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
m_handler.update();
|
||||
m_endpoint->update();
|
||||
value += (float)std::rand() / RAND_MAX;
|
||||
|
||||
m_velocity.x = glm::sin(value);
|
||||
@@ -81,7 +102,7 @@ public:
|
||||
player_input->set_y(0);
|
||||
player_input->set_z(m_velocity.z);
|
||||
player_move_mesg.set_allocated_input(player_input);
|
||||
auto r = m_handler.send(player_move_mesg);
|
||||
auto r = m_messages.send(m_server, player_move_mesg, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ add_subdirectory(tests)
|
||||
|
||||
file(GLOB FILES
|
||||
src/*.cpp
|
||||
src/messenger/*.cpp
|
||||
src/protocol/quicr/*.cpp
|
||||
src/frames/*.cpp
|
||||
)
|
||||
@@ -13,9 +12,6 @@ file(GLOB FILES
|
||||
file(GLOB HEADERS
|
||||
include/*.hpp
|
||||
include/exception/*.hpp
|
||||
include/io/*.hpp
|
||||
include/messenger/*.hpp
|
||||
include/packets/*.hpp
|
||||
include/metrics/*.hpp
|
||||
include/protocol/quicr/*.hpp
|
||||
)
|
||||
@@ -37,7 +33,10 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
spdlog::spdlog
|
||||
tw::io
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::quicr
|
||||
tl::expected
|
||||
Tracy::TracyClient
|
||||
TracyClient
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.hpp"
|
||||
#include "protocol/quicr/QuicrFrameType.hpp"
|
||||
#include "quicr/QuicrFrameType.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <google/protobuf/message.h>
|
||||
#include <span>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "Messenger.hpp"
|
||||
#include "NetworkError.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include "packets/LoginPacket.hpp"
|
||||
#include "protocol/quicr/QuicrConnection.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* Contains handlers for each message type. Calls this handler when message comes in.
|
||||
*/
|
||||
class MessageHandler {
|
||||
private:
|
||||
// std::optional<Messenger<std::byte, quicr::QuicrConnection>> m_quicr_messenger;
|
||||
// Messenger<std::byte, TcpStream> m_server_messenger;
|
||||
std::unique_ptr<quicr::QuicrEndpoint> m_quicr_endpoint;
|
||||
quicr::QuicrConnection* m_quicr_connection;
|
||||
|
||||
std::vector<std::function<tl::expected<void, NetworkError>(std::span<std::byte>)>> m_handlers;
|
||||
|
||||
std::unique_ptr<quicr::QuicrEndpoint> create_endpoint() {
|
||||
auto endpoint_r = quicr::QuicrEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
spdlog::error("Failed to create QuicrEndpoint: {}", endpoint_r.error().message());
|
||||
throw std::runtime_error("Failed to create QuicrEndpoint");
|
||||
}
|
||||
|
||||
return std::make_unique<quicr::QuicrEndpoint>(std::move(endpoint_r.value()));
|
||||
}
|
||||
|
||||
public:
|
||||
const bool is_connected() const {
|
||||
return m_quicr_connection->state() == quicr::QuicrConnectionState::Established;
|
||||
}
|
||||
|
||||
MessageHandler(MessageHandler&& m)
|
||||
// : m_server_messenger{std::move(m.m_server_messenger)},
|
||||
:
|
||||
m_handlers(std::move(m.m_handlers)),
|
||||
m_quicr_endpoint(std::move(m.m_quicr_endpoint)),
|
||||
m_quicr_connection(m.m_quicr_connection) {
|
||||
|
||||
}
|
||||
|
||||
MessageHandler(Address address) :
|
||||
m_quicr_endpoint(create_endpoint()),
|
||||
m_quicr_connection(m_quicr_endpoint->connect(address).value()),
|
||||
m_handlers(100) {
|
||||
spdlog::info("Connected to server at {}", address.to_string());
|
||||
}
|
||||
|
||||
|
||||
// MessageHandler(Messenger<std::byte, TcpStream>&& server_messenger) :
|
||||
// // m_server_messenger{std::move(server_messenger)},
|
||||
// m_quicr_connection(std::move(server_messenger.connection())),
|
||||
// m_handlers(100) {
|
||||
|
||||
// }
|
||||
|
||||
template<typename T>
|
||||
constexpr void set_handler(const std::function<void(T*)> handler) {
|
||||
PacketType type = Message<T>::value;
|
||||
m_handlers[type] = [handler, this](std::span<std::byte> data) -> tl::expected<void, NetworkError> {
|
||||
T result = {};
|
||||
|
||||
result.ParseFromArray(data.data(), data.size());
|
||||
// spdlog::info("Deserialized message [{}]: {}", (int32_t)Message<T>::value, result.DebugString());
|
||||
|
||||
handler(&result);
|
||||
// if(m_server_messenger.peek().has_value() && m_server_messenger.peek().value() == Message<T>::value) {
|
||||
// tl::expected<T, NetworkError> mesg = m_server_messenger.pop<T>(nullptr);
|
||||
// if(!mesg.has_value()) {
|
||||
// return tl::make_unexpected(mesg.error());
|
||||
// }
|
||||
|
||||
// handler(&mesg.value());
|
||||
// }
|
||||
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
constexpr void set_raw_handler(uint32_t type, const std::function<tl::expected<void, NetworkError>(std::span<std::byte>)> handler) {
|
||||
m_handlers[type] = handler;
|
||||
}
|
||||
|
||||
void update() {
|
||||
m_quicr_endpoint->poll();
|
||||
while(true) {
|
||||
std::vector<std::byte> buffer(64 * 1024);
|
||||
auto read_r = m_quicr_connection->read_into(buffer);
|
||||
|
||||
if(!read_r) {
|
||||
spdlog::error("Failed to read from QUICr stream: {}", read_r.error().message());
|
||||
break;
|
||||
}
|
||||
|
||||
if(*read_r == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t type = reinterpret_cast<uint32_t*>(buffer.data())[0];
|
||||
if(m_handlers[type] == nullptr) {
|
||||
spdlog::warn("Unknown message type: {}", type);
|
||||
throw std::runtime_error("Unknown message type: {}");
|
||||
break;
|
||||
}
|
||||
|
||||
auto handler_r = m_handlers[type](std::span<std::byte>(buffer.data(), *read_r).subspan(sizeof(uint32_t)));
|
||||
if(!handler_r) {
|
||||
spdlog::error("Handler error");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// while(m_server_messenger.peek().has_value() && m_server_messenger.peek().value().has_value()) {
|
||||
// std::optional<PacketType> type = m_server_messenger.peek().value();
|
||||
// if(type >= m_handlers.size() || m_handlers[type.value()] == nullptr) {
|
||||
// spdlog::warn("Unknown message type: {}", (int)type.value());
|
||||
// break;
|
||||
// }
|
||||
|
||||
// auto r = m_handlers[type.value()]();
|
||||
// if(!r) {
|
||||
// spdlog::error("Failed to handle message: {}", r.error().message());
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
template<std::derived_from<google::protobuf::Message> T>
|
||||
tl::expected<size_t, NetworkError> send(T& mesg) {
|
||||
std::string payload;
|
||||
if(!mesg.SerializeToString(&payload)) {
|
||||
spdlog::error("Failed to serialize message");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t length = payload.length();
|
||||
if(length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::byte> bytes(length + sizeof(uint32_t));
|
||||
|
||||
uint32_t type = Message<T>::value;
|
||||
auto payload_bytes = std::as_writable_bytes(std::span(payload));
|
||||
memcpy(bytes.data(), &type, sizeof(type));
|
||||
memcpy(bytes.data() + sizeof(uint32_t), payload_bytes.data(), payload_bytes.size());
|
||||
|
||||
auto send_r = m_quicr_connection->send_message(bytes, false);
|
||||
if(!send_r) {
|
||||
spdlog::error("Failed to send message: {}", send_r.error().message());
|
||||
return 0;
|
||||
}
|
||||
return payload_bytes.size();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <immintrin.h>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <google/protobuf/message.h>
|
||||
#include <google/protobuf/io/zero_copy_stream_impl.h>
|
||||
#include <tracy/Tracy.hpp>
|
||||
|
||||
#include "NetworkError.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include "MessageRegistry.hpp"
|
||||
#include "protocol/quicr/QuicrFrameType.hpp"
|
||||
#include "tl/expected.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
template<typename TData, std::derived_from<Write<TData>> TOutput>
|
||||
class Messenger {
|
||||
private:
|
||||
const uint32_t MAX_MESG_BODY_SIZE = 65536;
|
||||
const uint32_t MESG_MAGIC = 0x1DEADBEE;
|
||||
|
||||
TOutput m_stream;
|
||||
|
||||
std::optional<PacketType> m_next_packet_type;
|
||||
|
||||
bool m_is_skipping;
|
||||
uint32_t m_buffered_size;
|
||||
|
||||
size_t m_mesg_size;
|
||||
size_t m_read_head;
|
||||
std::vector<std::byte> m_input_buffer;
|
||||
|
||||
public:
|
||||
Messenger(Messenger && m) :
|
||||
m_stream(std::move(m.m_stream)),
|
||||
m_next_packet_type(m.m_next_packet_type),
|
||||
m_input_buffer(std::move(m.m_input_buffer)),
|
||||
m_buffered_size(m.m_buffered_size),
|
||||
m_is_skipping(m.m_is_skipping),
|
||||
m_mesg_size(m.m_mesg_size),
|
||||
m_read_head(m.m_read_head)
|
||||
{
|
||||
// m_stream.set_non_blocking();
|
||||
}
|
||||
|
||||
Messenger(TOutput&& stream) :
|
||||
m_stream(std::move(stream)),
|
||||
m_input_buffer(MAX_MESG_BODY_SIZE),
|
||||
m_buffered_size(0),
|
||||
m_is_skipping(false),
|
||||
m_mesg_size(0),
|
||||
m_read_head(0)
|
||||
{
|
||||
// m_stream.set_non_blocking();
|
||||
}
|
||||
|
||||
Messenger<TData, TOutput> operator=(const Messenger<TData, TOutput>&) = delete;
|
||||
|
||||
Messenger<TData, TOutput> operator=(Messenger<TData, TOutput>&& m) {
|
||||
m_stream = std::move(m.m_stream);
|
||||
m_next_packet_type = m.m_next_packet_type;
|
||||
m_input_buffer = std::move(m.m_input_buffer);
|
||||
m_buffered_size = m.m_buffered_size;
|
||||
m_is_skipping = m.m_is_skipping;
|
||||
m_mesg_size = m.m_mesg_size;
|
||||
m_read_head = m.m_read_head;
|
||||
}
|
||||
|
||||
template <std::derived_from<google::protobuf::Message> T>
|
||||
tl::expected<size_t, NetworkError> send(T &content) {
|
||||
ZoneScopedN("Messenger::send");
|
||||
auto id = (int32_t)Message<T>::value;
|
||||
|
||||
std::string payload;
|
||||
if(!content.SerializeToString(&payload)) {
|
||||
spdlog::error("Failed to serialize message");
|
||||
throw std::runtime_error("Serialization failed");
|
||||
}
|
||||
|
||||
// spdlog::info("Sending {}: {}", (int)Message<T>::value, content.DebugString());
|
||||
|
||||
int32_t length = payload.length();
|
||||
if(length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// append encoded id & length before payload and write it to the stream
|
||||
//
|
||||
const uint32_t HEADER_SIZE = 4 + 4 + 4 + 4;
|
||||
|
||||
std::string message;
|
||||
message.resize(HEADER_SIZE + payload.length());
|
||||
|
||||
const uint32_t magic = 0xDEADBEEF;
|
||||
const uint32_t frame_type = quicr::FrameType::StreamBase;
|
||||
|
||||
std::memcpy(message.data(), &magic, sizeof(magic));
|
||||
std::memcpy(message.data() + sizeof(magic), &frame_type, sizeof(frame_type));
|
||||
std::memcpy(message.data() + sizeof(frame_type) + sizeof(magic), &length, sizeof(length));
|
||||
std::memcpy(message.data() + sizeof(frame_type) + sizeof(magic) + sizeof(length), &id, sizeof(id));
|
||||
// std::memcpy(message.data() + sizeof(id) + sizeof(length), &MESG_MAGIC, sizeof(MESG_MAGIC));
|
||||
std::memcpy(message.data() + HEADER_SIZE, payload.data(), payload.length());
|
||||
|
||||
auto write_result = m_stream.write(std::as_writable_bytes(std::span(message)));
|
||||
if(!write_result.has_value()) {
|
||||
return tl::make_unexpected(write_result.error());
|
||||
}
|
||||
|
||||
return write_result.value();
|
||||
}
|
||||
|
||||
int32_t m_packet_peek_size = 0;
|
||||
|
||||
tl::expected<std::optional<PacketType>, NetworkError> peek() {
|
||||
ZoneScopedN("Messenger::peek");
|
||||
if(m_next_packet_type.has_value()) {
|
||||
return m_next_packet_type;
|
||||
}
|
||||
|
||||
if(m_read_head < 4) {
|
||||
auto result = m_stream.read_into(std::as_writable_bytes(std::span{(char*)m_input_buffer.data(), sizeof(PacketType) - m_read_head}));
|
||||
|
||||
if(!result.has_value()) {
|
||||
return tl::make_unexpected(result.error());
|
||||
}
|
||||
|
||||
m_read_head += result.value();
|
||||
|
||||
if(m_read_head < 4) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if(m_read_head < 8) {
|
||||
auto result = m_stream.read_into(std::as_writable_bytes(std::span{(char*)m_input_buffer.data() + m_read_head, 8 - m_read_head}));
|
||||
|
||||
if(!result.has_value()) {
|
||||
return tl::make_unexpected(result.error());
|
||||
}
|
||||
|
||||
m_read_head += result.value();
|
||||
|
||||
if(m_read_head < 8) {
|
||||
return {};
|
||||
}
|
||||
|
||||
m_mesg_size = *reinterpret_cast<uint32_t*>(m_input_buffer.data() + 4);
|
||||
}
|
||||
|
||||
if(m_read_head < m_mesg_size + 8) {
|
||||
if(m_mesg_size + 8 > m_input_buffer.size()) {
|
||||
return tl::make_unexpected(NetworkError(NetworkErrorType::NOT_ENOUGH_MEMORY));
|
||||
}
|
||||
|
||||
auto result = m_stream.read_into(std::as_writable_bytes(std::span{(char*)m_input_buffer.data() + m_read_head, m_mesg_size + 8 - m_read_head}));
|
||||
|
||||
if(!result.has_value()) {
|
||||
return tl::make_unexpected(result.error());
|
||||
}
|
||||
|
||||
m_read_head += result.value();
|
||||
|
||||
if(m_read_head < m_mesg_size + 8) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
m_next_packet_type = (PacketType)(*reinterpret_cast<int32_t*>(m_input_buffer.data()));
|
||||
return m_next_packet_type;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
tl::expected<T, NetworkError> pop(size_t* out_size) {
|
||||
ZoneScopedN("Messenger::pop");
|
||||
T result = {};
|
||||
|
||||
result.ParseFromArray(m_input_buffer.data() + 8, m_mesg_size);
|
||||
// spdlog::info("Received {}: {}", (int)m_next_packet_type.value(), result.DebugString());
|
||||
|
||||
m_read_head = 0;
|
||||
m_mesg_size = 0;
|
||||
m_next_packet_type = {};
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void skip() {
|
||||
|
||||
}
|
||||
|
||||
void clear() {
|
||||
m_next_packet_type = std::nullopt;
|
||||
|
||||
int message_length = 0;
|
||||
int size = sizeof(message_length);
|
||||
|
||||
// m_stream.read_exact(std::as_writable_bytes(std::span{&message_length, 1}));
|
||||
|
||||
std::vector<char> data(message_length);
|
||||
// m_stream.read_exact(std::as_writable_bytes(std::span{data.data(), (size_t)message_length}));
|
||||
|
||||
// m_input_buffer.reset();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "MessageRegistry.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
class MessengerDebugLog {
|
||||
public:
|
||||
MessengerDebugLog(MessengerDebugLog&& m) :
|
||||
m_log_file(std::move(m.m_log_file))
|
||||
{ }
|
||||
|
||||
MessengerDebugLog(const std::string& log_file_path);
|
||||
~MessengerDebugLog();
|
||||
|
||||
template<typename T>
|
||||
void log_send(const T& message) {
|
||||
spdlog::info("Sending [{}]: {}", (int)tw::Message<T>::value, message.DebugString());
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void log_recv(const T& message) {
|
||||
spdlog::info("Received [{}]: {}", (int)tw::Message<T>::value, message.DebugString());
|
||||
}
|
||||
|
||||
private:
|
||||
std::ofstream m_log_file;
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <print>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
template<typename T>
|
||||
struct AverageOp {
|
||||
uint32_t count;
|
||||
T sum;
|
||||
|
||||
AverageOp() :
|
||||
count(0),
|
||||
sum{} {
|
||||
}
|
||||
|
||||
void add(const T value) {
|
||||
sum += value;
|
||||
count++;
|
||||
}
|
||||
|
||||
T result() const {
|
||||
return count == 0 ? 0 : sum / count;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct SumOp {
|
||||
T sum;
|
||||
|
||||
void add(const T value) {
|
||||
sum += value;
|
||||
}
|
||||
|
||||
T result() const {
|
||||
return sum;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename Interval,
|
||||
typename Operation = SumOp<T>,
|
||||
typename Clock = std::chrono::steady_clock>
|
||||
class BucketMetric {
|
||||
|
||||
T m_min, m_max;
|
||||
|
||||
std::vector<T> m_metric;
|
||||
std::vector<uint32_t> m_bucket_idx;
|
||||
|
||||
Operation m_op;
|
||||
|
||||
uint32_t m_offset;
|
||||
uint32_t m_right, m_left;
|
||||
|
||||
std::string m_format;
|
||||
|
||||
const uint32_t get_bucket(Clock::time_point time_point) const {
|
||||
return std::chrono::floor<Interval>(time_point).time_since_epoch().count() - m_offset;
|
||||
}
|
||||
|
||||
public:
|
||||
BucketMetric(std::string format, uint32_t size) :
|
||||
m_metric(size),
|
||||
m_bucket_idx(size),
|
||||
m_right(0), m_left(0),
|
||||
m_offset(0),
|
||||
m_format(format)
|
||||
{
|
||||
m_offset = get_bucket(Clock::now());
|
||||
}
|
||||
|
||||
const T max() const {
|
||||
return m_max;
|
||||
}
|
||||
|
||||
const T min() const {
|
||||
return m_min;
|
||||
}
|
||||
|
||||
const std::string& format() const {
|
||||
return m_format;
|
||||
}
|
||||
|
||||
size_t max_size() const {
|
||||
return m_metric.size();
|
||||
}
|
||||
|
||||
void push(T value) {
|
||||
auto time = Clock::now();
|
||||
size_t bucket = get_bucket(time) % m_metric.size();
|
||||
size_t idx = get_bucket(time);
|
||||
|
||||
// set result to correct bucket
|
||||
if(m_right != bucket) {
|
||||
m_metric[m_right] = m_op.result();
|
||||
m_min = std::min(m_min, m_op.result());
|
||||
m_max = std::max(m_max, m_op.result());
|
||||
|
||||
m_bucket_idx[m_right] = idx++;
|
||||
m_right++;
|
||||
m_op = {};
|
||||
}
|
||||
|
||||
// reset all buckets until the required one
|
||||
for(; m_right != bucket; m_right = (m_right + 1) % m_metric.size()) {
|
||||
m_metric[m_right] = {};
|
||||
m_bucket_idx[m_right] = idx++;
|
||||
if(m_right == m_left) {
|
||||
m_left = (m_left + 1) % m_metric.size();
|
||||
}
|
||||
}
|
||||
|
||||
m_op.add(value);
|
||||
}
|
||||
|
||||
const size_t get_size() const {
|
||||
return m_right - m_left + (m_left > m_right ? m_metric.size() : 0);
|
||||
}
|
||||
|
||||
const T get(uint32_t idx) const {
|
||||
if(idx > get_size()) {
|
||||
throw std::invalid_argument("`idx` cannot be higher than buffer size");
|
||||
}
|
||||
|
||||
return m_metric[m_left + idx].result();
|
||||
}
|
||||
|
||||
std::span<T> get_head() {
|
||||
return std::span(m_metric).subspan(m_left, (m_right > m_left ? m_right : m_metric.size()));
|
||||
}
|
||||
|
||||
std::span<uint32_t> get_head_timeline() {
|
||||
return std::span(m_bucket_idx).subspan(m_left, (m_right > m_left ? m_right : m_bucket_idx.size()));
|
||||
}
|
||||
|
||||
std::span<T> get_tail() {
|
||||
if(m_right > m_left) {
|
||||
return std::span<T>();
|
||||
}
|
||||
|
||||
return std::span(m_metric).subspan(0, m_right);
|
||||
}
|
||||
|
||||
std::span<uint32_t> get_tail_timeline() {
|
||||
if(m_right > m_left) {
|
||||
return std::span<T>();
|
||||
}
|
||||
|
||||
return std::span(m_bucket_idx).subspan(0, m_right);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
}
|
||||
|
||||
std::optional<const TValue*> get(TKey key) const {
|
||||
for(size_t i = m_tail; i != m_head; (i++) % max_size()) {
|
||||
for(size_t i = m_tail; i != m_head; i = (i + 1) % max_size()) {
|
||||
if(m_buffer[i].first > key) {
|
||||
return {};
|
||||
}
|
||||
@@ -56,24 +56,25 @@ public:
|
||||
}
|
||||
|
||||
bool set(TKey key, const TValue& value) {
|
||||
if(key < m_buffer.at(m_tail).first) {
|
||||
// If buffer has entries and key is older than the oldest, reject it
|
||||
if(m_tail != m_head && key < m_buffer.at(m_tail).first) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int i = m_tail + 1;
|
||||
size_t i = (m_tail + 1) % max_size();
|
||||
if(m_tail != m_head) {
|
||||
for(i = m_tail + 1; i != m_head; i++) {
|
||||
for(i = (m_tail + 1) % max_size(); i != m_head; i = (i + 1) % max_size()) {
|
||||
if(m_buffer.at(i).first > key) {
|
||||
m_buffer[(i - 1) % max_size()] = std::make_pair(key, value);
|
||||
m_head++;
|
||||
m_buffer[(i - 1 + max_size()) % max_size()] = std::make_pair(key, value);
|
||||
m_head = (m_head + 1) % max_size();
|
||||
return true;
|
||||
} else {
|
||||
m_buffer[(i - 1) % max_size()] = m_buffer[i];
|
||||
m_buffer[(i - 1 + max_size()) % max_size()] = m_buffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_buffer[(i - 1) % max_size()] = std::make_pair(key, value);
|
||||
m_buffer[(i - 1 + max_size()) % max_size()] = std::make_pair(key, value);
|
||||
m_head = (m_head + 1) % max_size();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <cassert>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "BucketMetric.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include "MessageRegistry.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
struct NetworkSendInfo {
|
||||
PacketType message_type;
|
||||
bool is_sent_by_us;
|
||||
Address target;
|
||||
TimePoint timepoint;
|
||||
std::span<uint8_t> buffer;
|
||||
|
||||
NetworkSendInfo(
|
||||
PacketType message_type,
|
||||
bool is_sent_by_us,
|
||||
const Address& target,
|
||||
const std::span<uint8_t> buffer
|
||||
) :
|
||||
message_type(message_type),
|
||||
is_sent_by_us(is_sent_by_us),
|
||||
target(target),
|
||||
timepoint(std::chrono::steady_clock::now()),
|
||||
buffer(buffer)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class NetworkStatsLogger {
|
||||
private:
|
||||
|
||||
|
||||
std::vector<NetworkSendInfo> m_backlog;
|
||||
|
||||
std::vector<uint8_t> m_buffer;
|
||||
size_t m_left, m_right;
|
||||
|
||||
std::optional<std::ostream> m_output;
|
||||
|
||||
using Interval = std::chrono::seconds;
|
||||
|
||||
BucketMetric<uint32_t, Interval, AverageOp<uint32_t>> m_ping_metric;
|
||||
BucketMetric<uint32_t, Interval, SumOp<uint32_t>> m_outgoing;
|
||||
BucketMetric<uint32_t, Interval, SumOp<uint32_t>> m_incoming;
|
||||
|
||||
public:
|
||||
|
||||
NetworkStatsLogger() :
|
||||
m_backlog(10000, {MESSAGE_PACKET, false, Address({}, 0), {}}),
|
||||
m_buffer(1000000),
|
||||
m_left(0), m_right(0),
|
||||
m_ping_metric("ms", 1000),
|
||||
m_outgoing("b/s", 1000),
|
||||
m_incoming("b/s", 1000)
|
||||
{ }
|
||||
|
||||
void set_file_output(std::filesystem::path path);
|
||||
|
||||
size_t get_size() {
|
||||
return m_right - m_left + (m_right < m_left ? m_backlog.size() : 0);
|
||||
}
|
||||
|
||||
NetworkSendInfo& get_item(uint32_t idx) {
|
||||
return m_backlog[(m_left + idx) % m_backlog.size()];
|
||||
}
|
||||
|
||||
std::span<uint8_t> allocate_memory_for_buffer(size_t size) {
|
||||
uint32_t start = m_right;
|
||||
if(m_buffer.size() - m_right < size) {
|
||||
// throw away packets from the start to make space
|
||||
for(; m_backlog[m_left].buffer.data() < m_buffer.data() + start + size &&
|
||||
m_left != m_right; m_left = (m_left + 1) % m_buffer.size()) { }
|
||||
|
||||
start = 0;
|
||||
}
|
||||
|
||||
uint32_t end = start + size;
|
||||
return std::span<uint8_t>(m_buffer.begin() + start, m_buffer.begin() + end);
|
||||
}
|
||||
|
||||
// constexpr void log(PacketType message_type, bool is_sent, const Address& target, const ByteBuffer& content) {
|
||||
// std::span<uint8_t> span = allocate_memory_for_buffer(content.size());
|
||||
|
||||
// memcpy(span.data(), content.data().data(), content.size());
|
||||
|
||||
// m_right = (m_right + 1) % m_backlog.size();
|
||||
// m_backlog[m_right] = NetworkSendInfo(message_type, is_sent, target, span);
|
||||
// }
|
||||
|
||||
// constexpr void log_receive(
|
||||
// PacketType message_type,
|
||||
// const Address& from
|
||||
// ) {
|
||||
// log(message_type, false, from, content);
|
||||
// m_incoming.push(content.size());
|
||||
// }
|
||||
|
||||
// constexpr void log_send(
|
||||
// PacketType message_type,
|
||||
// const Address& to
|
||||
// ) {
|
||||
// log(message_type, true, to, content);
|
||||
// m_outgoing.push(content.size());
|
||||
// }
|
||||
|
||||
void log_ping(uint32_t ping) {
|
||||
m_ping_metric.push(ping);
|
||||
}
|
||||
|
||||
BucketMetric<uint32_t, Interval, AverageOp<uint32_t>>& ping(){
|
||||
return m_ping_metric;
|
||||
}
|
||||
|
||||
BucketMetric<uint32_t, Interval>& outgoing(){
|
||||
return m_outgoing;
|
||||
}
|
||||
|
||||
BucketMetric<uint32_t, Interval>& incoming(){
|
||||
return m_incoming;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Entity.pb.h"
|
||||
#include "PlayerMove.pb.h"
|
||||
#include "WorldState.pb.h"
|
||||
#include "Login.pb.h"
|
||||
|
||||
#include "Packet.hpp"
|
||||
#include "Serialization.hpp"
|
||||
|
||||
const int MAX_USERNAME_LENGTH = 128;
|
||||
|
||||
struct LoginPacket {
|
||||
uint32_t username_length;
|
||||
char username[MAX_USERNAME_LENGTH];
|
||||
};
|
||||
|
||||
// template<>
|
||||
// class Message<LoginPacket> {
|
||||
// public:
|
||||
// static constexpr PacketType value = LOGIN_REQUEST_MSG;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
template<>
|
||||
class tw::net::Serializer<LoginPacket> final {
|
||||
public:
|
||||
static bool serialize(Serialization& buffer, LoginPacket& value) {
|
||||
buffer.serialize(&value.username_length);
|
||||
buffer.serialize(value.username, value.username_length);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// inline void to_json(json& j, const LoginPacket& value) {
|
||||
// j = json{
|
||||
// {"username_length", value.username_length},
|
||||
// {"username", std::string(value.username, value.username_length)}
|
||||
// };
|
||||
// }
|
||||
|
||||
// inline void from_json(const json& j, LoginPacket& value) {
|
||||
// j.at("username_length").get_to(value.username_length);
|
||||
// j.at("username").get_to(value.username);
|
||||
// }
|
||||
|
||||
struct LoginStatusPacket {
|
||||
bool is_okay;
|
||||
|
||||
LoginStatusPacket() {
|
||||
}
|
||||
|
||||
LoginStatusPacket(bool is_okay) :
|
||||
is_okay(is_okay)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
class tw::net::Serializer<LoginStatusPacket> final {
|
||||
public:
|
||||
static bool serialize(Serialization& buffer, LoginStatusPacket& value) {
|
||||
return buffer.serialize(&value.is_okay);
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Serializers.hpp"
|
||||
|
||||
// #define PACKET(name) struct #name {
|
||||
|
||||
|
||||
|
||||
|
||||
// template<>
|
||||
// class tw::net::Serializer<const PacketType> final {
|
||||
// public:
|
||||
// static bool serialize(tw::net::Serialization& buffer, const PacketType& value) {
|
||||
// uint32_t v = value;
|
||||
// return buffer.serialize((uint32_t*)&v);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// template<>
|
||||
// class tw::net::Serializer<PacketType> final {
|
||||
// public:
|
||||
// static bool serialize(tw::net::Serialization& buffer, PacketType& value) {
|
||||
// return buffer.serialize((uint32_t*)&value);
|
||||
// }
|
||||
// };
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
namespace tw::net::quicr {
|
||||
|
||||
enum class QuicrErrorType {
|
||||
ConnectionClosed
|
||||
};
|
||||
|
||||
|
||||
struct QuicrError {
|
||||
public:
|
||||
QuicrError(QuicrErrorType type) : type_(type), message_(map_quicr_error_type(type)) {}
|
||||
QuicrError(QuicrErrorType type, std::string message) : type_(type), message_(std::move(message)) {}
|
||||
QuicrErrorType type() const { return type_; }
|
||||
|
||||
std::string message() const { return message_; }
|
||||
|
||||
private:
|
||||
static std::string map_quicr_error_type(QuicrErrorType type) {
|
||||
switch (type) {
|
||||
case QuicrErrorType::ConnectionClosed:
|
||||
return "ConnectionClosed";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string message_;
|
||||
QuicrErrorType type_;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
#include "NetworkError.hpp"
|
||||
#include "protocol/quicr/QuicrConnection.hpp"
|
||||
#include "tl/expected.hpp"
|
||||
|
||||
namespace tw::net::quicr {
|
||||
|
||||
class QuicrStream : Write<std::byte>, Read<std::byte> {
|
||||
QuicrConnection* m_connection;
|
||||
bool m_is_reliable;
|
||||
|
||||
public:
|
||||
QuicrStream(QuicrConnection* connection, bool is_reliable);
|
||||
|
||||
tl::expected<size_t, NetworkError> write(std::span<std::byte> data) override {
|
||||
auto send_r = m_connection->send_message(data, m_is_reliable);
|
||||
if(!send_r) {
|
||||
return tl::make_unexpected(NetworkError::from_errno(CONNECTION_RESET));
|
||||
}
|
||||
|
||||
return *send_r;
|
||||
}
|
||||
|
||||
tl::expected<size_t, NetworkError> read_into(std::span<std::byte> target) override {
|
||||
auto read_r = m_connection->read_into(target);
|
||||
if(!read_r) {
|
||||
return tl::make_unexpected(NetworkError::from_errno(CONNECTION_RESET));
|
||||
}
|
||||
|
||||
return *read_r;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#include "messenger/Messenger.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "messenger/MessengerDebugLog.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
MessengerDebugLog::MessengerDebugLog(const std::string& log_file_path) : m_log_file(log_file_path) {
|
||||
if (!m_log_file.is_open()) {
|
||||
throw std::runtime_error("Failed to open log file");
|
||||
}
|
||||
}
|
||||
|
||||
MessengerDebugLog::~MessengerDebugLog() {
|
||||
m_log_file.close();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user