Files
Starworld/src/OverteClient.cpp

1043 lines
44 KiB
C++

#include "OverteClient.hpp"
#include "NLPacketCodec.hpp"
#include <chrono>
#include <cmath>
#include <iostream>
#include <random>
#include <sstream>
#include <iomanip>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/matrix_decompose.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <zlib.h>
using namespace std::chrono_literals;
using namespace Overte;
// Minimal QDataStream-like writer (Big Endian) for Qt wire format
namespace {
struct QtStream {
std::vector<uint8_t> buf;
void writeUInt8(uint8_t v) { buf.push_back(v); }
void writeUInt16BE(uint16_t v) {
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>(v & 0xFF));
}
void writeUInt32BE(uint32_t v) {
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>(v & 0xFF));
}
void writeUInt64BE(uint64_t v) {
for (int i = 7; i >= 0; --i) buf.push_back(static_cast<uint8_t>((v >> (i * 8)) & 0xFF));
}
void writeInt32BE(int32_t v) {
writeUInt32BE(static_cast<uint32_t>(v));
}
void writeBytes(const uint8_t* d, size_t n) { buf.insert(buf.end(), d, d + n); }
void writeQByteArray(const std::vector<uint8_t>& a) { writeUInt32BE(static_cast<uint32_t>(a.size())); writeBytes(a.data(), a.size()); }
void writeQByteArrayFromString(const std::string& s) { std::vector<uint8_t> v(s.begin(), s.end()); writeQByteArray(v); }
void writeQString(const std::string& s) {
// QDataStream QString: quint32 length (chars), then UTF-16 BE code units
writeUInt32BE(static_cast<uint32_t>(s.size()));
for (unsigned char c : s) { writeUInt16BE(static_cast<uint16_t>(c)); }
}
static bool parseHex(const std::string& hex, uint64_t& out, size_t digits) {
if (hex.size() < digits) return false; out = 0; for (size_t i = 0; i < digits; ++i) {
char ch = hex[i]; uint8_t val;
if (ch >= '0' && ch <= '9') val = ch - '0';
else if (ch >= 'a' && ch <= 'f') val = ch - 'a' + 10;
else if (ch >= 'A' && ch <= 'F') val = ch - 'A' + 10;
else return false; out = (out << 4) | val; }
return true;
}
void writeQUuidFromString(const std::string& uuid) {
// UUID string xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
std::string hex; hex.reserve(32);
for (char c : uuid) if (c != '-') hex.push_back(c);
if (hex.size() != 32) { // write zeros
for (int i = 0; i < 16; ++i) buf.push_back(0); return;
}
uint64_t d1=0,d2=0,d3=0; // using 64 for parse then cast
parseHex(hex.substr(0,8), d1, 8);
parseHex(hex.substr(8,4), d2, 4);
parseHex(hex.substr(12,4), d3, 4);
writeUInt32BE(static_cast<uint32_t>(d1));
writeUInt16BE(static_cast<uint16_t>(d2));
writeUInt16BE(static_cast<uint16_t>(d3));
// remaining 8 bytes
for (int i = 0; i < 8; ++i) {
uint64_t byteVal=0; parseHex(hex.substr(16 + i*2, 2), byteVal, 2);
writeUInt8(static_cast<uint8_t>(byteVal & 0xFF));
}
}
};
static std::vector<uint8_t> qCompressLike(const std::vector<uint8_t>& input, int level = Z_BEST_SPEED) {
// Produce Qt-like qCompress payload: 4-byte big-endian uncompressed size + zlib deflate stream
uLongf destLen = compressBound(input.size());
std::vector<uint8_t> comp(destLen);
int rc = compress2(comp.data(), &destLen, input.data(), input.size(), level);
if (rc != Z_OK) { destLen = 0; }
comp.resize(destLen);
std::vector<uint8_t> out;
out.reserve(4 + comp.size());
// 4-byte big-endian uncompressed size
out.push_back(static_cast<uint8_t>((input.size() >> 24) & 0xFF));
out.push_back(static_cast<uint8_t>((input.size() >> 16) & 0xFF));
out.push_back(static_cast<uint8_t>((input.size() >> 8) & 0xFF));
out.push_back(static_cast<uint8_t>(input.size() & 0xFF));
out.insert(out.end(), comp.begin(), comp.end());
return out;
}
} // namespace
// Generate a simple UUID-like string for session identification
static std::string generateUUID() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 255);
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (int i = 0; i < 16; ++i) {
if (i == 4 || i == 6 || i == 8 || i == 10) ss << '-';
ss << std::setw(2) << dis(gen);
}
return ss.str();
}
bool OverteClient::connect() {
// Generate session UUID
m_sessionUUID = generateUUID();
std::cout << "[OverteClient] Session UUID: " << m_sessionUUID << std::endl;
// Check for authentication credentials from environment
const char* usernameEnv = std::getenv("OVERTE_USERNAME");
if (usernameEnv) m_username = usernameEnv;
if (!m_username.empty()) {
std::cout << "[OverteClient] Username present (signature auth not yet implemented)" << std::endl;
}
// Parse ws://host:port
std::string url = m_domainUrl;
if (url.empty()) url = "ws://127.0.0.1:40102";
if (url.rfind("ws://", 0) == 0) url = url.substr(5);
auto colon = url.find(':');
m_host = colon == std::string::npos ? url : url.substr(0, colon);
m_port = colon == std::string::npos ? 40102 : std::stoi(url.substr(colon + 1));
// Check for environment override for UDP port (domain server UDP port)
const char* portEnv = std::getenv("OVERTE_UDP_PORT");
int udpPort = portEnv ? std::atoi(portEnv) : 40104; // Default to 40104 for Overte domain UDP
std::cout << "[OverteClient] Connecting to domain at " << m_host
<< " (HTTP:" << m_port << ", UDP:" << udpPort << ")" << std::endl;
// Resolve host:port
addrinfo hints{}; hints.ai_socktype = SOCK_STREAM; hints.ai_family = AF_UNSPEC;
addrinfo* res = nullptr;
int gai = ::getaddrinfo(m_host.c_str(), std::to_string(m_port).c_str(), &hints, &res);
if (gai != 0) {
std::cerr << "[OverteClient] getaddrinfo failed for " << m_host << ":" << m_port << " - " << gai_strerror(gai) << std::endl;
} else {
// Attempt TCP reachability for diagnostics
int fd = -1; addrinfo* rp = res;
for (; rp; rp = rp->ai_next) {
fd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd == -1) continue;
::fcntl(fd, F_SETFL, O_NONBLOCK);
int c = ::connect(fd, rp->ai_addr, rp->ai_addrlen);
if (c == 0 || (c == -1 && errno == EINPROGRESS)) {
std::cout << "[OverteClient] TCP reachable (non-blocking) to " << m_host << ":" << m_port << std::endl;
::close(fd); fd = -1; break;
}
::close(fd); fd = -1;
}
::freeaddrinfo(res);
if (fd == -1) {
// Not necessarily fatal; mixers are UDP. Continue with UDP.
}
}
// Setup UDP to target (domain server UDP port)
addrinfo uhints{}; uhints.ai_socktype = SOCK_DGRAM; uhints.ai_family = AF_UNSPEC;
addrinfo* ures = nullptr;
int ugai = ::getaddrinfo(m_host.c_str(), std::to_string(udpPort).c_str(), &uhints, &ures);
if (ugai != 0) {
std::cerr << "[OverteClient] UDP resolve failed: " << gai_strerror(ugai) << std::endl;
} else {
for (addrinfo* rp = ures; rp; rp = rp->ai_next) {
m_udpFd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (m_udpFd == -1) continue;
::fcntl(m_udpFd, F_SETFL, O_NONBLOCK);
std::memcpy(&m_udpAddr, rp->ai_addr, rp->ai_addrlen);
m_udpAddrLen = rp->ai_addrlen;
m_udpReady = true;
std::cout << "[OverteClient] UDP socket ready for " << m_host << ":" << udpPort << std::endl;
break;
}
::freeaddrinfo(ures);
}
// Simulate successful connections to mixers.
m_connected = connectAvatarMixer() && connectEntityServer() && connectAudioMixer();
if (!m_connected) {
std::cerr << "OverteClient: failed to connect one or more mixers" << std::endl;
return false;
}
// Send domain connect request to initiate handshake
// Start with domain list request - simpler packet
std::cout << "[OverteClient] Initiating domain handshake..." << std::endl;
sendDomainConnectRequest();
sendDomainListRequest();
m_useSimulation = (std::getenv("STARWORLD_SIMULATE") != nullptr);
if (m_useSimulation) {
// Seed a few demo entities with different types and properties
OverteEntity cubeA;
cubeA.id = m_nextEntityId++;
cubeA.name = "CubeA";
cubeA.type = EntityType::Box;
cubeA.color = glm::vec3(1.0f, 0.3f, 0.3f); // Red cube
cubeA.dimensions = glm::vec3(0.2f, 0.2f, 0.2f);
cubeA.transform = glm::translate(glm::mat4(1.0f), glm::vec3(-0.5f, 1.5f, -2.0f));
OverteEntity sphereB;
sphereB.id = m_nextEntityId++;
sphereB.name = "SphereB";
sphereB.type = EntityType::Sphere;
sphereB.color = glm::vec3(0.3f, 1.0f, 0.3f); // Green sphere
sphereB.dimensions = glm::vec3(0.15f, 0.15f, 0.15f);
sphereB.transform = glm::translate(glm::mat4(1.0f), glm::vec3(0.5f, 1.5f, -2.0f));
OverteEntity modelC;
modelC.id = m_nextEntityId++;
modelC.name = "ModelC";
modelC.type = EntityType::Model;
modelC.color = glm::vec3(0.3f, 0.3f, 1.0f); // Blue tint
modelC.dimensions = glm::vec3(0.25f, 0.25f, 0.25f);
modelC.modelUrl = "https://example.com/model.glb"; // Placeholder
modelC.transform = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 1.2f, -2.0f));
m_entities.emplace(cubeA.id, cubeA);
m_entities.emplace(sphereB.id, sphereB);
m_entities.emplace(modelC.id, modelC);
m_updateQueue.push_back(cubeA.id);
m_updateQueue.push_back(sphereB.id);
m_updateQueue.push_back(modelC.id);
std::cout << "[OverteClient] Simulation mode enabled (STARWORLD_SIMULATE=1) with 3 demo entities" << std::endl;
} else {
std::cout << "[OverteClient] Waiting for entity packets from Overte server..." << std::endl;
std::cout << "[OverteClient] Tip: Set STARWORLD_SIMULATE=1 to enable demo entities" << std::endl;
}
return true;
}
bool OverteClient::connectAvatarMixer() {
// For now, consider UDP socket readiness as mixer connectivity proxy.
m_avatarMixer = m_udpReady;
return true;
}
bool OverteClient::connectEntityServer() {
// Entity server connection will be established after DomainList reply
// For now, create socket and bind to receive packets
m_entityFd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (m_entityFd == -1) {
std::cerr << "[OverteClient] Failed to create EntityServer socket: " << std::strerror(errno) << std::endl;
return false;
}
// Make non-blocking
::fcntl(m_entityFd, F_SETFL, O_NONBLOCK);
// Bind to ephemeral port (let OS choose) for receiving entity packets
sockaddr_in bindAddr{};
bindAddr.sin_family = AF_INET;
bindAddr.sin_addr.s_addr = INADDR_ANY;
bindAddr.sin_port = 0; // Let OS assign port
if (::bind(m_entityFd, reinterpret_cast<sockaddr*>(&bindAddr), sizeof(bindAddr)) == -1) {
std::cerr << "[OverteClient] Failed to bind EntityServer socket: " << std::strerror(errno) << std::endl;
::close(m_entityFd);
m_entityFd = -1;
return false;
}
// Get the assigned port
socklen_t addrLen = sizeof(bindAddr);
if (::getsockname(m_entityFd, reinterpret_cast<sockaddr*>(&bindAddr), &addrLen) == 0) {
std::cout << "[OverteClient] EntityServer socket bound to port " << ntohs(bindAddr.sin_port) << std::endl;
}
m_entityServer = true;
return true;
}
bool OverteClient::connectAudioMixer() {
// TODO: Connect AudioMixer for voice chat.
m_audioMixer = true;
return true;
}
void OverteClient::poll() {
if (!m_connected) return;
// Poll domain UDP socket for domain-level packets
if (m_udpReady && m_udpFd != -1) {
char buf[1500];
sockaddr_storage from{}; socklen_t fromlen = sizeof(from);
ssize_t r = ::recvfrom(m_udpFd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&from), &fromlen);
if (r > 0) {
std::cout << "[OverteClient] <<< Received domain packet (" << r << " bytes)" << std::endl;
// Hex dump first 32 bytes for debugging
std::cout << "[OverteClient] Hex: ";
for (int i = 0; i < std::min(32, (int)r); ++i) {
printf("%02x ", (unsigned char)buf[i]);
}
std::cout << std::endl;
parseDomainPacket(buf, static_cast<size_t>(r));
} else if (r < 0 && errno != EWOULDBLOCK && errno != EAGAIN) {
// Only log errors that aren't "would block"
static int errorCount = 0;
if (++errorCount <= 3) {
std::cerr << "[OverteClient] UDP recv error: " << strerror(errno) << std::endl;
}
}
// Send periodic ping to domain to keep connection alive
static auto lastPing = std::chrono::steady_clock::now();
static auto lastDomainList = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastPing).count() >= 1) {
sendPing(m_udpFd, m_udpAddr, m_udpAddrLen);
lastPing = now;
}
// Request domain list periodically if not connected
if (!m_domainConnected && std::chrono::duration_cast<std::chrono::seconds>(now - lastDomainList).count() >= 3) {
std::cout << "[OverteClient] Retrying domain handshake..." << std::endl;
sendDomainConnectRequest();
sendDomainListRequest();
lastDomainList = now;
}
}
// Parse entity server packets
parseNetworkPackets();
if (m_useSimulation) {
// Simulate entity transforms changing slightly over time.
static auto t0 = std::chrono::steady_clock::now();
const float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - t0).count();
for (auto& [id, e] : m_entities) {
const float r = 0.25f + 0.05f * static_cast<float>(id);
const float x = std::cos(t * 0.5f + static_cast<float>(id)) * r;
const float z = std::sin(t * 0.5f + static_cast<float>(id)) * r;
e.transform = glm::translate(glm::mat4(1.0f), glm::vec3{x, 1.25f, z});
m_updateQueue.push_back(id);
}
}
}
void OverteClient::parseNetworkPackets() {
// Read from EntityServer socket
if (m_entityServerReady && m_entityFd != -1) {
char buf[1500];
sockaddr_storage from{}; socklen_t fromlen = sizeof(from);
ssize_t r = ::recvfrom(m_entityFd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&from), &fromlen);
if (r > 0) {
std::cout << "[OverteClient] EntityServer packet received (" << r << " bytes, type=0x"
<< std::hex << (int)(unsigned char)buf[0] << std::dec << ")" << std::endl;
parseEntityPacket(buf, static_cast<size_t>(r));
}
}
}
void OverteClient::parseDomainPacket(const char* data, size_t len) {
if (len < 6) return; // NLPacket header is minimum 6 bytes
// Parse NLPacket header
NLPacket::Header header;
const uint8_t* udata = reinterpret_cast<const uint8_t*>(data);
if (!NLPacket::parseHeader(udata, len, header)) {
std::cerr << "[OverteClient] Failed to parse NLPacket header" << std::endl;
return;
}
PacketType packetType = NLPacket::getType(udata, len);
std::cout << "[OverteClient] Domain packet type: " << static_cast<int>(packetType)
<< " (0x" << std::hex << static_cast<int>(packetType) << std::dec << ")"
<< " version: " << (int)header.version << std::endl;
// Payload starts after header (6 bytes base, +2 if has source ID)
const char* payload = data + 6; // Assuming no source ID for now
size_t payloadLen = len - 6;
switch (packetType) {
case PacketType::DomainList:
handleDomainListReply(payload, payloadLen);
break;
case PacketType::DomainConnectionDenied:
handleDomainConnectionDenied(payload, payloadLen);
break;
case PacketType::DomainServerRequireDTLS:
std::cout << "[OverteClient] Domain server requires DTLS (not yet implemented)" << std::endl;
break;
case PacketType::PingReply:
// Keep-alive ping reply
std::cout << "[OverteClient] Ping reply received" << std::endl;
break;
default:
std::cout << "[OverteClient] Unknown domain packet type: " << static_cast<int>(packetType) << std::endl;
break;
}
}
void OverteClient::parseEntityPacket(const char* data, size_t len) {
// Overte packet structure (simplified):
// - Byte 0: PacketType
// - Following bytes: payload (varies by type)
if (len < 1) return;
unsigned char packetType = static_cast<unsigned char>(data[0]);
// Entity packet types
const unsigned char PACKET_TYPE_ENTITY_ADD = 0x10;
const unsigned char PACKET_TYPE_ENTITY_EDIT = 0x11;
const unsigned char PACKET_TYPE_ENTITY_ERASE = 0x12;
const unsigned char PACKET_TYPE_ENTITY_QUERY = 0x15;
const unsigned char PACKET_TYPE_OCTREE_STATS = 0x16;
const unsigned char PACKET_TYPE_ENTITY_DATA = 0x41; // Bulk entity data response
switch (packetType) {
case PACKET_TYPE_ENTITY_DATA:
case PACKET_TYPE_ENTITY_ADD: {
// EntityAdd packet structure (enhanced):
// [type:u8][id:u64][name:null-terminated][position:3xf32][rotation:4xf32][dimensions:3xf32][model_url:null-terminated][texture_url:null-terminated][color:3xf32]
if (len < 9) break; // need at least 1+8 bytes
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
// Parse name (null-terminated string after ID)
size_t offset = 9;
std::string name;
while (offset < len && data[offset] != '\0') {
name += data[offset++];
}
offset++; // skip null terminator
if (name.empty()) name = "Entity_" + std::to_string(entityId);
// Parse position (vec3 - 3 floats)
glm::vec3 position(0.0f, 1.5f, -2.0f); // Default
if (offset + 12 <= len) {
std::memcpy(&position.x, data + offset, 4);
std::memcpy(&position.y, data + offset + 4, 4);
std::memcpy(&position.z, data + offset + 8, 4);
offset += 12;
}
// Parse rotation (quaternion - 4 floats: x, y, z, w)
glm::quat rotation(1.0f, 0.0f, 0.0f, 0.0f); // Identity (w, x, y, z in glm)
if (offset + 16 <= len) {
float qx, qy, qz, qw;
std::memcpy(&qx, data + offset, 4);
std::memcpy(&qy, data + offset + 4, 4);
std::memcpy(&qz, data + offset + 8, 4);
std::memcpy(&qw, data + offset + 12, 4);
rotation = glm::quat(qw, qx, qy, qz); // glm uses w first
offset += 16;
}
// Parse dimensions/scale (vec3 - 3 floats)
glm::vec3 dimensions(0.1f, 0.1f, 0.1f); // Default
if (offset + 12 <= len) {
std::memcpy(&dimensions.x, data + offset, 4);
std::memcpy(&dimensions.y, data + offset + 4, 4);
std::memcpy(&dimensions.z, data + offset + 8, 4);
offset += 12;
}
// Parse model URL (null-terminated string)
std::string modelUrl;
while (offset < len && data[offset] != '\0') {
modelUrl += data[offset++];
}
offset++; // skip null terminator
// Parse texture URL (null-terminated string)
std::string textureUrl;
while (offset < len && data[offset] != '\0') {
textureUrl += data[offset++];
}
offset++; // skip null terminator
// Parse color (vec3 RGB - 3 floats 0-1)
glm::vec3 color(1.0f, 1.0f, 1.0f); // Default white
if (offset + 12 <= len) {
std::memcpy(&color.r, data + offset, 4);
std::memcpy(&color.g, data + offset + 4, 4);
std::memcpy(&color.b, data + offset + 8, 4);
offset += 12;
}
// Parse entity type (optional, u8)
EntityType entityType = EntityType::Box; // Default
if (offset + 1 <= len) {
uint8_t typeCode = static_cast<uint8_t>(data[offset++]);
// Map Overte entity type codes to our enum
// 0=Unknown, 1=Box, 2=Sphere, 3=Model, etc.
if (typeCode <= static_cast<uint8_t>(EntityType::Material)) {
entityType = static_cast<EntityType>(typeCode);
}
}
// Build transform matrix from position, rotation, scale
glm::mat4 transform = glm::mat4(1.0f);
transform = glm::translate(transform, position);
transform = transform * glm::mat4_cast(rotation);
transform = glm::scale(transform, dimensions);
// Create entity with all properties
OverteEntity entity;
entity.id = entityId;
entity.name = name;
entity.transform = transform;
entity.type = entityType;
entity.modelUrl = modelUrl;
entity.textureUrl = textureUrl;
entity.color = color;
entity.dimensions = dimensions;
entity.alpha = 1.0f; // Default fully opaque
m_entities[entityId] = entity;
m_updateQueue.push_back(entityId);
std::cout << "[OverteClient] Entity added: " << name << " (id=" << entityId << ")" << std::endl;
std::cout << " Type: " << static_cast<int>(entityType) << std::endl;
std::cout << " Position: (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl;
std::cout << " Rotation: (" << rotation.x << ", " << rotation.y << ", " << rotation.z << ", " << rotation.w << ")" << std::endl;
std::cout << " Dimensions: (" << dimensions.x << ", " << dimensions.y << ", " << dimensions.z << ")" << std::endl;
std::cout << " Color: RGB(" << color.r << ", " << color.g << ", " << color.b << ")" << std::endl;
if (!modelUrl.empty()) {
std::cout << " Model: " << modelUrl << std::endl;
}
if (!textureUrl.empty()) {
std::cout << " Texture: " << textureUrl << std::endl;
}
break;
}
case PACKET_TYPE_ENTITY_EDIT: {
// EntityEdit packet: [type:u8][id:u64][flags:u8][property data...]
if (len < 10) break; // Need type + id + flags
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
uint8_t flags = data[9];
size_t offset = 10;
const uint8_t HAS_POSITION = 0x01;
const uint8_t HAS_ROTATION = 0x02;
const uint8_t HAS_DIMENSIONS = 0x04;
auto it = m_entities.find(entityId);
if (it != m_entities.end()) {
glm::vec3 position(0.0f);
glm::quat rotation(1.0f, 0.0f, 0.0f, 0.0f);
glm::vec3 dimensions(1.0f);
// Extract current transform
glm::vec3 scale;
glm::quat currentRot;
glm::vec3 currentPos;
glm::vec3 skew;
glm::vec4 perspective;
glm::decompose(it->second.transform, scale, currentRot, currentPos, skew, perspective);
position = currentPos;
rotation = currentRot;
dimensions = scale;
// Update based on flags
if (flags & HAS_POSITION) {
if (offset + 12 <= len) {
std::memcpy(&position.x, data + offset, 4);
std::memcpy(&position.y, data + offset + 4, 4);
std::memcpy(&position.z, data + offset + 8, 4);
offset += 12;
}
}
if (flags & HAS_ROTATION) {
if (offset + 16 <= len) {
float qx, qy, qz, qw;
std::memcpy(&qx, data + offset, 4);
std::memcpy(&qy, data + offset + 4, 4);
std::memcpy(&qz, data + offset + 8, 4);
std::memcpy(&qw, data + offset + 12, 4);
rotation = glm::quat(qw, qx, qy, qz);
offset += 16;
}
}
if (flags & HAS_DIMENSIONS) {
if (offset + 12 <= len) {
std::memcpy(&dimensions.x, data + offset, 4);
std::memcpy(&dimensions.y, data + offset + 4, 4);
std::memcpy(&dimensions.z, data + offset + 8, 4);
offset += 12;
}
}
// Rebuild transform
glm::mat4 transform = glm::mat4(1.0f);
transform = glm::translate(transform, position);
transform = transform * glm::mat4_cast(rotation);
transform = glm::scale(transform, dimensions);
it->second.transform = transform;
m_updateQueue.push_back(entityId);
std::cout << "[OverteClient] Entity edited: id=" << entityId << " (flags=0x" << std::hex << (int)flags << std::dec << ")" << std::endl;
if (flags & HAS_POSITION) {
std::cout << " New position: (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl;
}
if (flags & HAS_ROTATION) {
std::cout << " New rotation: (" << rotation.x << ", " << rotation.y << ", " << rotation.z << ", " << rotation.w << ")" << std::endl;
}
if (flags & HAS_DIMENSIONS) {
std::cout << " New dimensions: (" << dimensions.x << ", " << dimensions.y << ", " << dimensions.z << ")" << std::endl;
}
}
break;
}
case PACKET_TYPE_ENTITY_ERASE: {
// EntityErase packet: u64 entityID
if (len < 9) break;
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
auto it = m_entities.find(entityId);
if (it != m_entities.end()) {
m_entities.erase(it);
m_deleteQueue.push_back(entityId);
std::cout << "[OverteClient] Entity erased: id=" << entityId << std::endl;
}
break;
}
case PACKET_TYPE_OCTREE_STATS:
std::cout << "[OverteClient] Received octree stats" << std::endl;
break;
default:
std::cout << "[OverteClient] Unknown entity packet type: 0x" << std::hex << (int)packetType << std::dec << std::endl;
break;
}
}
void OverteClient::handleDomainListReply(const char* data, size_t len) {
// DomainList packet format (from Overte NodeList.cpp):
// 1. Domain UUID (16 bytes)
// 2. Session UUID (16 bytes)
// 3. Domain Local ID (16 bits)
// 4. Permissions (32 bits)
// 5. Authenticated (bool)
// 6. Number of nodes (varies)
// 7. Node data...
std::cout << "[OverteClient] DomainList reply received (" << len << " bytes)" << std::endl;
if (len < 37) { // Min: 16 (UUID) + 16 (session) + 2 (localID) + 4 (perms) + 1 (auth) = 39, but let's check for 37
std::cout << "[OverteClient] DomainList packet too short" << std::endl;
return;
}
size_t offset = 0;
// Read domain UUID
if (offset + 16 > len) return;
char domainUUID[33];
snprintf(domainUUID, sizeof(domainUUID),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
(unsigned char)data[offset], (unsigned char)data[offset+1],
(unsigned char)data[offset+2], (unsigned char)data[offset+3],
(unsigned char)data[offset+4], (unsigned char)data[offset+5],
(unsigned char)data[offset+6], (unsigned char)data[offset+7],
(unsigned char)data[offset+8], (unsigned char)data[offset+9],
(unsigned char)data[offset+10], (unsigned char)data[offset+11],
(unsigned char)data[offset+12], (unsigned char)data[offset+13],
(unsigned char)data[offset+14], (unsigned char)data[offset+15]);
offset += 16;
std::cout << "[OverteClient] Domain UUID: " << domainUUID << std::endl;
// Read session UUID
if (offset + 16 > len) return;
char sessionUUID[33];
snprintf(sessionUUID, sizeof(sessionUUID),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
(unsigned char)data[offset], (unsigned char)data[offset+1],
(unsigned char)data[offset+2], (unsigned char)data[offset+3],
(unsigned char)data[offset+4], (unsigned char)data[offset+5],
(unsigned char)data[offset+6], (unsigned char)data[offset+7],
(unsigned char)data[offset+8], (unsigned char)data[offset+9],
(unsigned char)data[offset+10], (unsigned char)data[offset+11],
(unsigned char)data[offset+12], (unsigned char)data[offset+13],
(unsigned char)data[offset+14], (unsigned char)data[offset+15]);
offset += 16;
std::cout << "[OverteClient] Session UUID: " << sessionUUID << std::endl;
// Read domain local ID (16-bit)
if (offset + 2 > len) return;
uint16_t localID = ntohs(*reinterpret_cast<const uint16_t*>(data + offset));
offset += 2;
std::cout << "[OverteClient] Local ID: " << localID << std::endl;
// Read permissions (32-bit)
if (offset + 4 > len) return;
uint32_t permissions = ntohl(*reinterpret_cast<const uint32_t*>(data + offset));
offset += 4;
std::cout << "[OverteClient] Permissions: 0x" << std::hex << permissions << std::dec << std::endl;
// Read authenticated flag
if (offset + 1 > len) return;
bool authenticated = data[offset++];
std::cout << "[OverteClient] Authenticated: " << (authenticated ? "yes" : "no") << std::endl;
// Now mark as connected since we got a valid DomainList
m_domainConnected = true;
// Read number of nodes - this might be encoded as QDataStream int
if (offset + 4 > len) return;
uint32_t numNodes = ntohl(*reinterpret_cast<const uint32_t*>(data + offset));
offset += 4;
std::cout << "[OverteClient] Number of assignment clients: " << numNodes << std::endl;
// If numNodes seems too large, it might be a different encoding
if (numNodes > 100) {
std::cout << "[OverteClient] Warning: Suspicious node count, packet format may be incorrect" << std::endl;
// Dump remaining bytes for analysis
std::cout << "[OverteClient] Remaining bytes: ";
for (size_t i = offset - 4; i < std::min(offset + 20, len); i++) {
printf("%02x ", (unsigned char)data[i]);
}
std::cout << std::endl;
return;
}
for (uint32_t i = 0; i < numNodes && offset < len; ++i) {
// Read NodeType
if (offset + 1 > len) break;
unsigned char nodeType = static_cast<unsigned char>(data[offset++]);
// Skip UUID (16 bytes)
if (offset + 16 > len) break;
offset += 16;
// Read public socket address
if (offset + sizeof(sockaddr_in) > len) break;
sockaddr_in publicAddr;
std::memcpy(&publicAddr, data + offset, sizeof(sockaddr_in));
offset += sizeof(sockaddr_in);
// Skip local socket (same size)
if (offset + sizeof(sockaddr_in) > len) break;
offset += sizeof(sockaddr_in);
// NodeType values from Overte:
// 0 = DomainServer, 1 = EntityServer, 2 = Agent, 3 = AudioMixer,
// 4 = AvatarMixer, 5 = AssetServer, 6 = MessagesMixer, 7 = EntityScriptServer
const unsigned char NODE_TYPE_ENTITY_SERVER = 1;
const unsigned char NODE_TYPE_AVATAR_MIXER = 4;
const unsigned char NODE_TYPE_AUDIO_MIXER = 3;
char addrStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &publicAddr.sin_addr, addrStr, sizeof(addrStr));
int port = ntohs(publicAddr.sin_port);
const char* nodeTypeName = "Unknown";
switch (nodeType) {
case 0: nodeTypeName = "DomainServer"; break;
case NODE_TYPE_ENTITY_SERVER: nodeTypeName = "EntityServer"; break;
case 2: nodeTypeName = "Agent"; break;
case NODE_TYPE_AUDIO_MIXER: nodeTypeName = "AudioMixer"; break;
case NODE_TYPE_AVATAR_MIXER: nodeTypeName = "AvatarMixer"; break;
case 5: nodeTypeName = "AssetServer"; break;
case 6: nodeTypeName = "MessagesMixer"; break;
case 7: nodeTypeName = "EntityScriptServer"; break;
}
std::cout << "[OverteClient] Assignment: " << nodeTypeName
<< " at " << addrStr << ":" << port << std::endl;
if (nodeType == NODE_TYPE_ENTITY_SERVER) {
// Update EntityServer connection to use discovered address
std::cout << "[OverteClient] Connecting to EntityServer at " << addrStr << ":" << port << std::endl;
// Update target address for EntityServer
sockaddr_in* entityAddr = reinterpret_cast<sockaddr_in*>(&m_entityAddr);
entityAddr->sin_family = AF_INET;
entityAddr->sin_port = publicAddr.sin_port;
entityAddr->sin_addr = publicAddr.sin_addr;
m_entityAddrLen = sizeof(sockaddr_in);
m_entityServerReady = true;
// Send EntityQuery to request all entities
sendEntityQuery();
}
}
}
void OverteClient::handleDomainConnectionDenied(const char* data, size_t len) {
std::cerr << "[OverteClient] Domain connection DENIED!" << std::endl;
// Parse reason if available
if (len > 0) {
std::string reason(data, len);
std::cerr << "[OverteClient] Reason: " << reason << std::endl;
}
m_domainConnected = false;
}
void OverteClient::sendDomainConnectRequest() {
if (!m_udpReady || m_udpFd == -1) return;
// Create NLPacket with DomainConnectRequest type and correct version
NLPacket packet(PacketType::DomainConnectRequest, PacketVersions::DomainConnectRequest_SocketTypes, true);
packet.setSequenceNumber(m_sequenceNumber++);
// Build payload using Qt wire format (match Overte's NodeList.cpp structure exactly)
QtStream qs;
// 1. UUID
qs.writeQUuidFromString(m_sessionUUID);
// 2. Protocol signature (QByteArray)
auto protocolSig = NLPacket::computeProtocolVersionSignature();
qs.writeQByteArray(protocolSig);
// 3. Hardware/MAC address (QString) - empty if unknown
std::string macAddr = "";
qs.writeQString(macAddr);
// 4. Machine fingerprint (QUuid)
qs.writeQUuidFromString(m_sessionUUID);
// 5. Compressed system info (QByteArray)
std::string sysJson = "{\"computer\":{\"OS\":\"Linux\"},\"cpus\":[{\"model\":\"Stardust\"}],\"memory\":4096,\"nics\":[],\"gpus\":[],\"displays\":[]}";
std::vector<uint8_t> sysBytes(sysJson.begin(), sysJson.end());
auto sysCompressed = qCompressLike(sysBytes, Z_BEST_SPEED);
qs.writeQByteArray(sysCompressed);
// 6. Connect reason (quint32) - 0 = Unknown
qs.writeUInt32BE(0);
// 7. Previous connection uptime (quint64) - 0 for first connection
qs.writeUInt64BE(0);
// 8. Current timestamp in microseconds (quint64) as lastPingTimestamp
auto nowUs = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
qs.writeUInt64BE(static_cast<uint64_t>(nowUs));
// 9. Node type / owner type (NodeType_t)
qs.writeUInt8(static_cast<uint8_t>('I')); // Agent
// Determine local UDP socket address/port (bind address if needed)
uint32_t localIPv4 = 0x7F000001; // 127.0.0.1 fallback
uint16_t localPort = 0;
sockaddr_storage localSs{}; socklen_t localLen = sizeof(localSs);
if (::getsockname(m_udpFd, reinterpret_cast<sockaddr*>(&localSs), &localLen) == 0) {
if (localSs.ss_family == AF_INET) {
auto* sin = reinterpret_cast<sockaddr_in*>(&localSs);
localIPv4 = ntohl(sin->sin_addr.s_addr);
localPort = ntohs(sin->sin_port);
}
}
// Helper lambda to write QHostAddress (IPv4) in QDataStream format: [protocol:quint8=1][IPv4:quint32]
auto writeQHostAddressIPv4 = [&qs](uint32_t hostOrderIPv4){
// QDataStream for QHostAddress writes a protocol tag (quint8).
// QAbstractSocket::NetworkLayerProtocol: AnyIPProtocol=0, IPv4Protocol=1, IPv6Protocol=2.
// We want IPv4Protocol = 1.
qs.writeUInt8(1);
qs.writeUInt32BE(hostOrderIPv4);
};
// 10. Public socket: type (quint8) + SockAddr (QHostAddress + quint16 port, WITHOUT socket type per SockAddr QDataStream operator)
qs.writeUInt8(1); // SocketType::UDP
writeQHostAddressIPv4(localIPv4); // using local as placeholder for public
qs.writeUInt16BE(localPort); // actual local port (might be 0 if not yet bound)
// 11. Local socket: type (quint8) + SockAddr
qs.writeUInt8(1); // SocketType::UDP
writeQHostAddressIPv4(localIPv4);
qs.writeUInt16BE(localPort);
// 12. Node types of interest (QList<NodeType_t>)
// Write as Qt container: size (qint32) + elements (quint8) -- include a few mixers we want
// Typical Interface requests at least AvatarMixer, AudioMixer, EntityServer
const uint8_t interestList[] = { static_cast<uint8_t>('W'), /* AvatarMixer */ static_cast<uint8_t>('M'), /* AudioMixer */ static_cast<uint8_t>('o') /* EntityServer */ };
qs.writeInt32BE(static_cast<int32_t>(sizeof(interestList)));
for (auto b : interestList) qs.writeUInt8(b);
// 13. Place name (QString) - empty
qs.writeQString("");
// Append payload to packet
if (!qs.buf.empty()) packet.write(qs.buf.data(), qs.buf.size());
const auto& data = packet.getData();
ssize_t s = ::sendto(m_udpFd, data.data(), data.size(), 0,
reinterpret_cast<sockaddr*>(&m_udpAddr), m_udpAddrLen);
if (s > 0) {
std::cout << "[OverteClient] DomainConnectRequest sent (" << s << " bytes, seq=" << (m_sequenceNumber-1) << ")" << std::endl;
std::cout << "[OverteClient] Session UUID: " << m_sessionUUID << std::endl;
// Print MD5 signature in hex for diff against reference Overte client
std::ostringstream md5hex; md5hex << std::hex << std::setfill('0');
for (uint8_t byte : protocolSig) md5hex << std::setw(2) << (int)byte;
// Base64 encode MD5 for comparison with Overte's protocolVersionsSignatureBase64()
auto base64Encode = [](const std::vector<uint8_t>& in){
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out; out.reserve(((in.size()+2)/3)*4);
size_t i=0; while(i<in.size()){ uint32_t val=0; int bytes=0; for(int j=0;j<3;++j){ val <<=8; if(i<in.size()){ val|=in[i++]; ++bytes; } }
int pad = 3 - bytes; for(int k=0;k<4-pad;++k){ int idx = (val >> (18 - k*6)) & 0x3F; out.push_back(tbl[idx]); }
for(int k=0;k<pad;++k) out.push_back('='); }
return out; };
std::string md5Base64 = base64Encode(protocolSig);
std::cout << "[OverteClient] Protocol signature: " << protocolSig.size() << " bytes (MD5)" << std::endl;
std::cout << "[OverteClient] Protocol signature (hex): " << md5hex.str() << std::endl;
std::cout << "[OverteClient] Protocol signature (base64): " << md5Base64 << std::endl;
// Hex dump first 64 bytes
std::cout << "[OverteClient] >>> NLPacket Hex: ";
for (size_t i = 0; i < std::min(size_t(64), data.size()); ++i) {
printf("%02x ", data[i]);
}
std::cout << std::endl;
} else {
std::cerr << "[OverteClient] Failed to send domain connect request: " << strerror(errno) << std::endl;
}
}
void OverteClient::sendDomainListRequest() {
// Send DomainList request packet using NLPacket format
if (!m_udpReady || m_udpFd == -1) return;
// Create NLPacket with DomainListRequest type and correct version
NLPacket packet(PacketType::DomainListRequest, PacketVersions::DomainListRequest_SocketTypes, true);
packet.setSequenceNumber(m_sequenceNumber++);
// DomainListRequest has no payload, just the header
const auto& data = packet.getData();
ssize_t s = ::sendto(m_udpFd, data.data(), data.size(), 0,
reinterpret_cast<sockaddr*>(&m_udpAddr), m_udpAddrLen);
if (s > 0) {
std::cout << "[OverteClient] DomainListRequest sent (seq=" << (m_sequenceNumber-1) << ")" << std::endl;
} else {
std::cerr << "[OverteClient] Failed to send domain list request: " << strerror(errno) << std::endl;
}
}
void OverteClient::sendPing(int fd, const sockaddr_storage& addr, socklen_t addrLen) {
// Create NLPacket for Ping with correct version
NLPacket packet(PacketType::Ping, PacketVersions::Ping_IncludeConnectionID, false);
packet.setSequenceNumber(m_sequenceNumber++);
// Add timestamp (microseconds since epoch)
auto now = std::chrono::system_clock::now();
auto micros = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
packet.writeUInt64(micros);
// Ping type (0 = local, 1 = public)
packet.writeUInt8(0);
const auto& data = packet.getData();
ssize_t s = ::sendto(fd, data.data(), data.size(), 0,
reinterpret_cast<const sockaddr*>(&addr), addrLen);
if (s < 0 && errno != EWOULDBLOCK && errno != EAGAIN) {
std::cerr << "[OverteClient] Ping send failed: " << strerror(errno) << std::endl;
}
}
void OverteClient::sendEntityQuery() {
if (m_entityFd < 0 || !m_entityServerReady) return;
const unsigned char PACKET_TYPE_ENTITY_QUERY = 0x15;
// EntityQuery packet structure (simplified):
// [PacketType:u8][ConicalViews:bool][CameraFrustum if ConicalViews=true]
// For simplicity, send with ConicalViews=false to request all entities
std::vector<char> packet;
packet.push_back(static_cast<char>(PACKET_TYPE_ENTITY_QUERY));
packet.push_back(0); // ConicalViews = false
// With ConicalViews=false, we're requesting all entities
// Additional octree query parameters can be added here
ssize_t sent = sendto(m_entityFd, packet.data(), packet.size(), 0,
reinterpret_cast<const sockaddr*>(&m_entityAddr), m_entityAddrLen);
if (sent > 0) {
std::cout << "[OverteClient] Sent EntityQuery to EntityServer" << std::endl;
} else {
std::cerr << "[OverteClient] Failed to send EntityQuery: " << strerror(errno) << std::endl;
}
}
void OverteClient::sendMovementInput(const glm::vec3& linearVelocity) {
(void)linearVelocity; // TODO: send to avatar mixer
}
std::vector<OverteEntity> OverteClient::consumeUpdatedEntities() {
std::vector<OverteEntity> out;
out.reserve(m_updateQueue.size());
for (auto id : m_updateQueue) {
auto it = m_entities.find(id);
if (it != m_entities.end()) out.push_back(it->second);
}
m_updateQueue.clear();
return out;
}
std::vector<std::uint64_t> OverteClient::consumeDeletedEntities() {
std::vector<std::uint64_t> out;
out.swap(m_deleteQueue); // efficient clear
return out;
}