Compare commits

...

57 Commits

Author SHA1 Message Date
MayaTheShy
38cb81560c feat: add DiscoveredDomain struct and discoverDomains function for metaverse domain discovery 2025-11-08 19:11:31 -05:00
MayaTheShy
f533e5da63 feat: add domain discovery feature to query metaverse directories for public domains 2025-11-08 19:11:23 -05:00
MayaTheShy
2f900c775e feat: implement domain discovery functionality using libcurl for HTTP GET requests 2025-11-08 19:11:15 -05:00
MayaTheShy
31f235dbce fix: add CURL package dependency and link it to the stardust-overte-client executable 2025-11-08 19:11:05 -05:00
MayaTheShy
a489d6f613 fix: update sendDomainConnectRequest to use correct IPv4Protocol tag and enhance protocol signature logging 2025-11-08 19:05:46 -05:00
MayaTheShy
eaf56daac2 fix: update default version for unspecified packet types to match Overte PacketHeaders.cpp 2025-11-08 19:02:53 -05:00
MayaTheShy
acb9747e4a fix: update sendDomainConnectRequest to use correct IPv4Protocol tag in QDataStream format 2025-11-08 19:01:45 -05:00
MayaTheShy
dd04d483ef feat: enhance sendDomainConnectRequest to include local socket details and improve logging of protocol signature 2025-11-08 19:00:12 -05:00
MayaTheShy
8c4cfb5261 feat: update sendDomainConnectRequest to use QUuid and adjust data types for connection parameters 2025-11-08 18:51:45 -05:00
MayaTheShy
3cd660e7e1 feat: add debug output for protocol version signature computation 2025-11-08 18:36:47 -05:00
MayaTheShy
3e657c1e02 refactor: remove debug output from computeProtocolVersionSignature function 2025-11-08 18:33:09 -05:00
MayaTheShy
765d63bbb5 feat: add writeInt32BE method to QtStream for writing 32-bit integers in Big Endian format 2025-11-08 18:32:01 -05:00
MayaTheShy
b10ee9f434 feat: enhance DomainConnectRequest payload structure and logging details 2025-11-08 18:31:47 -05:00
MayaTheShy
a976107332 refactor: remove redundant debug output in ensureVersionTable function 2025-11-08 18:29:16 -05:00
MayaTheShy
c1b109d95a feat: add additional debug output for entity and audio packet processing in ensureVersionTable function 2025-11-08 18:26:23 -05:00
MayaTheShy
47e427cc26 feat: simplify debug output for protocol version signature computation in NLPacket 2025-11-08 18:23:38 -05:00
MayaTheShy
b0c428bb66 feat: add debug output for parsed avatar version values in ensureVersionTable function 2025-11-08 18:22:54 -05:00
MayaTheShy
a391c1814e fix: correct avatar query packet version name in ensureVersionTable function 2025-11-08 18:22:36 -05:00
MayaTheShy
a0e2231137 feat: add debug output for protocol version signature computation in NLPacket 2025-11-08 18:21:15 -05:00
MayaTheShy
8c541ed1c5 feat: update dummy variable types and ensureVersionTable call in computeProtocolVersionSignature 2025-11-08 18:19:30 -05:00
MayaTheShy
c65fe9e278 feat: add avatar query packet version handling to version table 2025-11-08 18:19:17 -05:00
MayaTheShy
19cac1dbd8 feat: add version handling for DomainSettings packet type 2025-11-08 18:18:12 -05:00
MayaTheShy
f7e75bb706 feat: update version handling for entity particle spin and avatar query conical in NLPacket 2025-11-08 18:17:58 -05:00
MayaTheShy
313acd4d69 feat: add entity particle spin handling to version table initialization 2025-11-08 18:17:02 -05:00
MayaTheShy
3a35ac86da feat: add avatar query conical to version table initialization 2025-11-08 18:16:42 -05:00
MayaTheShy
27c13044df feat: add avatar query conical and entity particle spin to version table initialization 2025-11-08 18:16:20 -05:00
MayaTheShy
0659f6fdb7 feat: add new version handling for entity particle spin and avatar query conical 2025-11-08 18:16:03 -05:00
MayaTheShy
e29ef3a051 fix: correct string literal in endPos calculation for parseEnumValues function 2025-11-08 18:14:32 -05:00
MayaTheShy
043a9e1798 fix: add ZLIB dependency to CMakeLists.txt for proper linking 2025-11-08 18:13:19 -05:00
MayaTheShy
5b242fdea6 feat: implement QtStream for packet serialization and update domain connect request payload 2025-11-08 18:13:11 -05:00
MayaTheShy
69b388e4fe feat: enhance NLPacketCodec with additional packet types and version handling 2025-11-08 18:09:03 -05:00
MayaTheShy
25441fcc1f fix: clarify that username is not sent in DomainConnectRequest for signature-based authentication 2025-11-08 18:01:03 -05:00
MayaTheShy
cd58444bec fix: remove password prompt and clarify authentication method in run_with_auth.sh 2025-11-08 17:59:26 -05:00
MayaTheShy
2adef096eb fix: remove password handling from authentication methods in documentation 2025-11-08 17:59:22 -05:00
MayaTheShy
01bc7a2519 fix: simplify authentication logging by removing password handling 2025-11-08 17:59:14 -05:00
MayaTheShy
2050c9b478 fix: clarify username comment for future authentication improvements 2025-11-08 17:59:06 -05:00
MayaTheShy
b42df944c1 fix: update number of packet types in computeProtocolVersionSignature to match Overte 2025-11-08 17:56:14 -05:00
MayaTheShy
35034d5de6 fix: update default version for unspecified packet types to match Overte 2025-11-08 17:55:35 -05:00
MayaTheShy
182a5c23de feat: add OpenSSL dependency to CMakeLists for secure networking support 2025-11-08 17:53:54 -05:00
MayaTheShy
aab3d069f6 feat: enhance DomainConnectRequest packet structure with UUID, protocol signature, and system info 2025-11-08 17:53:14 -05:00
MayaTheShy
6685bf1579 feat: add methods for computing protocol version signature and retrieving version for packet type 2025-11-08 17:53:00 -05:00
MayaTheShy
a61e2bb7c4 feat: add protocol version handling and signature computation to NLPacket 2025-11-08 17:52:52 -05:00
MayaTheShy
332018952d feat: add DomainServerRequireDTLS packet type to NLPacket protocol 2025-11-08 17:50:29 -05:00
MayaTheShy
ea6caa60d6 feat: improve NLPacket header parsing and logging in parseDomainPacket 2025-11-08 17:50:21 -05:00
MayaTheShy
d80df50030 feat: enhance domain packet parsing with NLPacket header validation and improved logging 2025-11-08 17:49:56 -05:00
MayaTheShy
6ced91125a feat: move type definitions for PacketVersion, LocalID, and SequenceNumber to improve code organization 2025-11-08 17:49:30 -05:00
MayaTheShy
03d2c965b6 feat: update NLPacket creation in sendPing to use correct version for Ping 2025-11-08 17:46:29 -05:00
MayaTheShy
e54dea19a9 feat: update NLPacket creation in sendDomainConnectRequest and sendDomainListRequest with correct versions 2025-11-08 17:46:20 -05:00
MayaTheShy
336f1b25b7 feat: update PacketType enum and add PacketVersions for enhanced protocol handling 2025-11-08 17:46:11 -05:00
MayaTheShy
2a50d7a85b feat: include <string> header in NLPacketCodec.hpp for string handling 2025-11-08 17:43:00 -05:00
MayaTheShy
747f1ded6a feat: include <cstring> and <string> headers in NLPacketCodec.cpp 2025-11-08 17:42:02 -05:00
MayaTheShy
a8c76649b6 feat: integrate NLPacket protocol for domain connection requests and responses 2025-11-08 17:41:44 -05:00
MayaTheShy
65d25a1bca feat: include NLPacketCodec header in OverteClient for packet handling 2025-11-08 17:39:56 -05:00
MayaTheShy
c463b8ea40 feat: disable Overte networking library option and include NLPacketCodec in executable 2025-11-08 17:39:39 -05:00
MayaTheShy
9b3f43a605 feat: add NLPacket protocol implementation for Overte domain communication 2025-11-08 17:39:29 -05:00
MayaTheShy
4150738be4 feat: implement OverteNetworkClient class for domain connectivity 2025-11-08 17:38:34 -05:00
MayaTheShy
e5e79c7b4a feat: enable Overte networking library support in CMake configuration 2025-11-08 17:37:52 -05:00
11 changed files with 1213 additions and 94 deletions

View File

@@ -6,8 +6,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(USE_OVERTE_SDK "Link against Overte SDK if available" OFF)
option(USE_STARDUST_SDK "Link against StardustXR SDK if available" OFF)
option(USE_OVERTE_NETWORKING "Link against Overte networking library" OFF) # Disabled, using custom impl
find_package(glm REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
# Find Qt5 for Overte networking library
if(USE_OVERTE_NETWORKING)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
set(CMAKE_AUTOMOC ON)
endif()
add_executable(stardust-overte-client
src/main.cpp
@@ -15,9 +24,32 @@ add_executable(stardust-overte-client
src/OverteClient.cpp
src/SceneSync.cpp
src/InputHandler.cpp
src/NLPacketCodec.cpp
src/DomainDiscovery.cpp
)
target_link_libraries(stardust-overte-client PRIVATE glm::glm)
find_package(CURL REQUIRED)
target_link_libraries(stardust-overte-client PRIVATE glm::glm ZLIB::ZLIB CURL::libcurl)
target_link_libraries(stardust-overte-client PRIVATE OpenSSL::Crypto)
# Link Overte networking library
if(USE_OVERTE_NETWORKING)
# Add Overte include directories
target_include_directories(stardust-overte-client PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/overte-src/libraries/networking/src
${CMAKE_CURRENT_SOURCE_DIR}/third_party/overte-src/libraries/shared/src
${CMAKE_CURRENT_SOURCE_DIR}/third_party/overte-src/libraries/platform/src
)
# Link against precompiled Overte networking library
target_link_libraries(stardust-overte-client PRIVATE
/opt/overte/lib/libnetworking.so
Qt5::Core
Qt5::Network
)
target_compile_definitions(stardust-overte-client PRIVATE HAVE_OVERTE_NETWORKING=1)
endif()
if(USE_OVERTE_SDK)
find_package(Overte QUIET)

View File

@@ -51,28 +51,26 @@ This will prompt you for username and password.
### Method 2: Environment Variables
```bash
OVERTE_USERNAME="your_username" OVERTE_PASSWORD="your_password" ./build/stardust-overte-client
OVERTE_USERNAME="your_username" ./build/stardust-overte-client
```
### Method 3: Export Variables
```bash
export OVERTE_USERNAME="your_username"
export OVERTE_PASSWORD="your_password"
./build/stardust-overte-client
```
## Configuration
- **OVERTE_USERNAME**: Your Overte domain username
- **OVERTE_PASSWORD**: Your Overte domain password or access token
- **OVERTE_USERNAME**: Your Overte account username (optional; signature-based auth not yet implemented)
- **OVERTE_UDP_PORT**: Domain server UDP port (default: 40104)
- **STARWORLD_SIMULATE**: Set to "1" to enable simulation mode with demo entities
## Troubleshooting
### No Response from Domain Server
### Protocol mismatch or denial
**This is expected!** The current implementation doesn't include the NLPacket protocol layer that Overte requires. Use the test environment instead:
If you see "Protocol version mismatch" or denial messages, this is due to incomplete protocol implementation (version signature mismatch and missing signature-based auth). Use the test environment instead:
```bash
# Works perfectly - bypasses domain server

View File

@@ -6,13 +6,9 @@ echo "=============================="
echo ""
echo -n "Username: "
read username
echo -n "Password: "
read -s password
echo "(Password no longer sent; Overte uses keypair signatures. Username is optional for now.)"
echo ""
echo ""
export OVERTE_USERNAME="$username"
export OVERTE_PASSWORD="$password"
echo "Connecting to Overte domain..."
./build/stardust-overte-client

148
src/DomainDiscovery.cpp Normal file
View File

@@ -0,0 +1,148 @@
#include "DomainDiscovery.hpp"
#include <string>
#include <vector>
#include <sstream>
#include <optional>
#include <cstdlib>
#include <cstring>
// Minimal libcurl-based GET
#include <curl/curl.h>
namespace {
struct Buffer { std::string data; };
size_t write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* b = reinterpret_cast<Buffer*>(userdata);
b->data.append(ptr, size * nmemb);
return size * nmemb;
}
std::optional<std::string> httpGet(const std::string& url, long timeoutMs = 3000) {
CURL* curl = curl_easy_init();
if (!curl) return std::nullopt;
Buffer buf;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeoutMs);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
// Optional auth header from env if needed
struct curl_slist* headers = nullptr;
if (const char* token = std::getenv("METAVERSE_TOKEN")) {
std::string h = std::string("Authorization: Bearer ") + token;
headers = curl_slist_append(headers, h.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
CURLcode rc = curl_easy_perform(curl);
long code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
if (headers) curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (rc != CURLE_OK || code < 200 || code >= 300) return std::nullopt;
return buf.data;
}
// Very small JSON helpers (avoid adding a full JSON lib):
// Extract values for keys we care about with a permissive search.
static std::vector<std::string> findAllStrings(const std::string& json, const std::string& key) {
std::vector<std::string> out;
std::string needle = '"' + key + '"';
size_t pos = 0;
while ((pos = json.find(needle, pos)) != std::string::npos) {
size_t colon = json.find(':', pos + needle.size()); if (colon == std::string::npos) break;
size_t quote1 = json.find('"', colon + 1); if (quote1 == std::string::npos) break;
if (json[quote1-1] == '\\') { pos = quote1 + 1; continue; }
size_t quote2 = json.find('"', quote1 + 1); if (quote2 == std::string::npos) break;
if (quote2 > quote1) {
out.emplace_back(json.substr(quote1 + 1, quote2 - quote1 - 1));
}
pos = quote2 + 1;
}
return out;
}
static std::vector<int> findAllInts(const std::string& json, const std::string& key) {
std::vector<int> out;
std::string needle = '"' + key + '"';
size_t pos = 0;
while ((pos = json.find(needle, pos)) != std::string::npos) {
size_t colon = json.find(':', pos + needle.size()); if (colon == std::string::npos) break;
size_t start = json.find_first_of("-0123456789", colon + 1); if (start == std::string::npos) break;
size_t end = json.find_first_not_of("0123456789", start + ((json[start] == '-') ? 1 : 0));
std::string num = json.substr(start, end - start);
try { out.emplace_back(std::stoi(num)); } catch (...) {}
pos = end;
}
return out;
}
// Heuristic: map fields from common metaverse JSONs
// Vircadia/Overte often expose entries with fields like name, network_address, domain, ice_server_address, port, etc.
std::vector<DiscoveredDomain> parseDomains(const std::string& json) {
std::vector<DiscoveredDomain> out;
auto names = findAllStrings(json, "name");
auto hostsA = findAllStrings(json, "network_address");
auto hostsB = findAllStrings(json, "ice_server_address");
auto hostsC = findAllStrings(json, "domain");
auto httpPorts = findAllInts(json, "http_port");
auto udpPorts = findAllInts(json, "udp_port");
// Gather candidates from each host list
auto addHostList = [&](const std::vector<std::string>& hosts) {
for (size_t i = 0; i < hosts.size(); ++i) {
DiscoveredDomain d;
d.name = (i < names.size()) ? names[i] : std::string();
d.networkHost = hosts[i];
d.httpPort = (i < httpPorts.size() && httpPorts[i] > 0) ? httpPorts[i] : 40102;
d.udpPort = (i < udpPorts.size() && udpPorts[i] > 0) ? udpPorts[i] : 40104;
out.emplace_back(std::move(d));
}
};
addHostList(hostsA);
addHostList(hostsB);
addHostList(hostsC);
// Dedup by host:port
std::vector<DiscoveredDomain> dedup;
for (auto& d : out) {
bool exists = false;
for (auto& x : dedup) {
if (x.networkHost == d.networkHost && x.httpPort == d.httpPort && x.udpPort == d.udpPort) { exists = true; break; }
}
if (!exists && !d.networkHost.empty()) dedup.emplace_back(std::move(d));
}
return dedup;
}
} // namespace
std::vector<DiscoveredDomain> discoverDomains(int maxDomains) {
std::vector<DiscoveredDomain> result;
curl_global_init(CURL_GLOBAL_DEFAULT);
// Allow override of endpoint via env
std::vector<std::string> endpoints;
if (const char* custom = std::getenv("METAVERSE_DISCOVERY_URL")) {
endpoints.emplace_back(custom);
}
// Common candidate endpoints
endpoints.emplace_back("https://metaverse.vircadia.com/api/domains");
endpoints.emplace_back("https://metaverse.overte.org/api/domains");
endpoints.emplace_back("https://overte.org/api/domains");
for (const auto& url : endpoints) {
auto body = httpGet(url);
if (!body) continue;
auto list = parseDomains(*body);
for (auto& d : list) {
result.emplace_back(std::move(d));
if ((int)result.size() >= maxDomains) break;
}
if ((int)result.size() >= maxDomains) break;
}
curl_global_cleanup();
return result;
}

15
src/DomainDiscovery.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
// Simple domain record discovered from metaverse API
struct DiscoveredDomain {
std::string name; // Friendly name if available
std::string networkHost; // Hostname or IP
int httpPort{40102}; // Control/HTTP port (defaults)
int udpPort{40104}; // UDP domain port
};
// Fetch a list of candidate domains. Non-fatal if empty.
// Implementation attempts several known metaverse endpoints.
std::vector<DiscoveredDomain> discoverDomains(int maxDomains = 25);

497
src/NLPacketCodec.cpp Normal file
View File

@@ -0,0 +1,497 @@
// NLPacketCodec.cpp
#include "NLPacketCodec.hpp"
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <openssl/md5.h>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <algorithm>
#include <iostream>
namespace Overte {
// Control bit masks for sequence number field
static constexpr uint32_t CONTROL_BIT_MASK = 0x80000000; // Bit 31
static constexpr uint32_t RELIABLE_BIT_MASK = 0x40000000; // Bit 30
static constexpr uint32_t MESSAGE_BIT_MASK = 0x20000000; // Bit 29
static constexpr uint32_t OBFUSCATION_MASK = 0x18000000; // Bits 27-28
static constexpr uint32_t SEQUENCE_NUMBER_MASK = 0x07FFFFFF; // Bits 0-26
NLPacket::NLPacket(PacketType type, PacketVersion version, bool isReliable)
: m_type(type)
, m_version(version)
, m_isReliable(isReliable)
{
// Determine header size (sourced packets have LocalID)
m_isSourced = false; // Most client packets aren't sourced
m_headerSize = m_isSourced ? SOURCED_HEADER_SIZE : BASE_HEADER_SIZE;
// Reserve space for header
m_data.resize(m_headerSize);
writeHeader();
}
void NLPacket::writeHeader() {
size_t offset = 0;
// Write sequence number and flags
uint32_t seqAndFlags = m_sequenceNumber & SEQUENCE_NUMBER_MASK;
if (m_isReliable) {
seqAndFlags |= RELIABLE_BIT_MASK;
}
// Convert to network byte order
uint32_t netSeqAndFlags = htonl(seqAndFlags);
std::memcpy(m_data.data() + offset, &netSeqAndFlags, sizeof(uint32_t));
offset += sizeof(uint32_t);
// Write packet type
m_data[offset++] = static_cast<uint8_t>(m_type);
// Write version
m_data[offset++] = m_version;
// Write source ID if sourced
if (m_isSourced) {
uint16_t netSourceID = htons(m_sourceID);
std::memcpy(m_data.data() + offset, &netSourceID, sizeof(uint16_t));
offset += sizeof(uint16_t);
}
}
void NLPacket::write(const void* data, size_t size) {
const uint8_t* bytes = static_cast<const uint8_t*>(data);
m_data.insert(m_data.end(), bytes, bytes + size);
}
void NLPacket::writeUInt8(uint8_t value) {
m_data.push_back(value);
}
void NLPacket::writeUInt16(uint16_t value) {
uint16_t netValue = htons(value);
write(&netValue, sizeof(netValue));
}
void NLPacket::writeUInt32(uint32_t value) {
uint32_t netValue = htonl(value);
write(&netValue, sizeof(netValue));
}
void NLPacket::writeUInt64(uint64_t value) {
// Network byte order for 64-bit (big-endian)
uint64_t netValue =
((value & 0xFF00000000000000ULL) >> 56) |
((value & 0x00FF000000000000ULL) >> 40) |
((value & 0x0000FF0000000000ULL) >> 24) |
((value & 0x000000FF00000000ULL) >> 8) |
((value & 0x00000000FF000000ULL) << 8) |
((value & 0x0000000000FF0000ULL) << 24) |
((value & 0x000000000000FF00ULL) << 40) |
((value & 0x00000000000000FFULL) << 56);
write(&netValue, sizeof(netValue));
}
void NLPacket::writeString(const std::string& str, bool nullTerminated) {
write(str.data(), str.size());
if (nullTerminated) {
writeUInt8(0);
}
}
void NLPacket::setSequenceNumber(SequenceNumber seq) {
m_sequenceNumber = seq & SEQUENCE_NUMBER_MASK;
writeHeader();
}
void NLPacket::setSourceID(LocalID id) {
m_sourceID = id;
m_isSourced = true;
// Resize if needed
if (m_headerSize != SOURCED_HEADER_SIZE) {
m_headerSize = SOURCED_HEADER_SIZE;
m_data.resize(m_headerSize);
}
writeHeader();
}
bool NLPacket::parseHeader(const uint8_t* data, size_t size, Header& header) {
if (size < BASE_HEADER_SIZE) {
return false;
}
size_t offset = 0;
// Read sequence and flags
uint32_t netSeqAndFlags;
std::memcpy(&netSeqAndFlags, data + offset, sizeof(uint32_t));
header.sequenceAndFlags = ntohl(netSeqAndFlags);
offset += sizeof(uint32_t);
// Read packet type
header.type = static_cast<PacketType>(data[offset++]);
// Read version
header.version = data[offset++];
// Read source ID if present (check if packet is sourced)
if (size >= SOURCED_HEADER_SIZE) {
uint16_t netSourceID;
std::memcpy(&netSourceID, data + offset, sizeof(uint16_t));
header.sourceID = ntohs(netSourceID);
} else {
header.sourceID = NULL_LOCAL_ID;
}
return true;
}
PacketType NLPacket::getType(const uint8_t* data, size_t size) {
if (size < sizeof(uint32_t) + 1) {
return PacketType::Unknown;
}
return static_cast<PacketType>(data[sizeof(uint32_t)]);
}
// --- Helpers to parse Overte header enums to ensure exact version numbers ---
static std::string readFileToString(const std::string& path) {
std::ifstream in(path);
if (!in.is_open()) return {};
std::ostringstream ss; ss << in.rdbuf();
return ss.str();
}
static std::unordered_map<std::string, int> parseEnumValues(const std::string& content, const std::string& enumName) {
std::unordered_map<std::string, int> values;
std::string startToken = "enum class " + enumName;
auto startPos = content.find(startToken);
if (startPos == std::string::npos) return values;
auto bracePos = content.find('{', startPos);
if (bracePos == std::string::npos) return values;
auto endPos = content.find("};", bracePos);
if (endPos == std::string::npos) return values;
std::string body = content.substr(bracePos + 1, endPos - bracePos - 1);
int current = -1;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line)) {
// strip comments
auto cpos = line.find("//");
if (cpos != std::string::npos) line = line.substr(0, cpos);
// trim
auto notspace = [](int ch){ return !std::isspace(ch); };
line.erase(line.begin(), std::find_if(line.begin(), line.end(), notspace));
line.erase(std::find_if(line.rbegin(), line.rend(), notspace).base(), line.end());
if (line.empty()) continue;
// split by comma; a line may have trailing comma
auto comma = line.find(',');
std::string token = (comma == std::string::npos) ? line : line.substr(0, comma);
// handle assignments
auto eq = token.find('=');
std::string name = token;
if (eq != std::string::npos) {
name = token.substr(0, eq);
std::string val = token.substr(eq + 1);
// trim name
name.erase(name.begin(), std::find_if(name.begin(), name.end(), notspace));
name.erase(std::find_if(name.rbegin(), name.rend(), notspace).base(), name.end());
// trim val
val.erase(val.begin(), std::find_if(val.begin(), val.end(), notspace));
val.erase(std::find_if(val.rbegin(), val.rend(), notspace).base(), val.end());
// numeric (no hex used in these enums)
try { current = std::stoi(val); } catch (...) { continue; }
} else {
// trim name
name.erase(name.begin(), std::find_if(name.begin(), name.end(), notspace));
name.erase(std::find_if(name.rbegin(), name.rend(), notspace).base(), name.end());
current = current + 1;
}
if (!name.empty()) values[name] = current;
}
return values;
}
static int parsePacketTypeCount(const std::string& content) {
// Count identifiers in PacketTypeEnum::Value until NUM_PACKET_TYPE
auto pos = content.find("enum class Value : uint8_t");
if (pos == std::string::npos) return 106; // fallback
auto brace = content.find('{', pos);
if (brace == std::string::npos) return 106;
auto end = content.find("NUM_PACKET_TYPE", brace);
if (end == std::string::npos) return 106;
std::string body = content.substr(brace + 1, end - brace - 1);
int count = 0;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line)) {
auto cpos = line.find("//"); if (cpos != std::string::npos) line = line.substr(0, cpos);
auto notspace = [](int ch){ return !std::isspace(ch); };
line.erase(line.begin(), std::find_if(line.begin(), line.end(), notspace));
line.erase(std::find_if(line.rbegin(), line.rend(), notspace).base(), line.end());
if (line.empty()) continue;
if (line.find('=') != std::string::npos) {
// handle explicit value lines (rare in this enum)
count++;
} else if (line.find(',') != std::string::npos) {
count++;
}
}
return count; // this should equal NUM_PACKET_TYPE
}
static void ensureVersionTable(uint8_t& vAvatarRemoveAttachments,
uint8_t& vAvatarTraitsAck,
uint8_t& vEntityLastPacket,
uint8_t& vEntityParticleSpin,
uint8_t& vAssetBakingTextureMeta,
uint8_t& vEntityScriptClientCallable,
uint8_t& vEntityQueryCbor,
uint8_t& vAvatarQueryConical,
uint8_t& vDomainServerAddedNodeSocketTypes,
uint8_t& vDomainListSocketTypes,
uint8_t& vDomainListRequestSocketTypes,
uint8_t& vDomainConnectionDeniedExtraInfo,
uint8_t& vPingIncludeConnID,
uint8_t& vIcePingSendPeerID,
uint8_t& vAudioStopInjectors,
int& numPacketTypes)
{
static bool inited = false;
static uint8_t s_vAvatarRemoveAttachments, s_vAvatarTraitsAck, s_vEntityLastPacket,
s_vEntityParticleSpin, s_vAssetBakingTextureMeta, s_vEntityScriptClientCallable, s_vEntityQueryCbor,
s_vAvatarQueryConical, s_vDomainServerAddedNodeSocketTypes, s_vDomainListSocketTypes,
s_vDomainListRequestSocketTypes, s_vDomainConnectionDeniedExtraInfo,
s_vPingIncludeConnID, s_vIcePingSendPeerID, s_vAudioStopInjectors;
static int s_numPacketTypes;
if (!inited) {
std::string path = "third_party/overte-src/libraries/networking/src/udt/PacketHeaders.h";
auto content = readFileToString(path);
if (!content.empty()) {
auto avatar = parseEnumValues(content, "AvatarMixerPacketVersion");
auto entity = parseEnumValues(content, "EntityVersion");
auto asset = parseEnumValues(content, "AssetServerPacketVersion");
auto entScript = parseEnumValues(content, "EntityScriptCallMethodVersion");
auto entQuery = parseEnumValues(content, "EntityQueryPacketVersion");
auto avatarQuery = parseEnumValues(content, "AvatarQueryVersion");
auto domAdded = parseEnumValues(content, "DomainServerAddedNodeVersion");
auto domList = parseEnumValues(content, "DomainListVersion");
auto domListReq = parseEnumValues(content, "DomainListRequestVersion");
auto domDenied = parseEnumValues(content, "DomainConnectionDeniedVersion");
auto ping = parseEnumValues(content, "PingVersion");
auto icePing = parseEnumValues(content, "IcePingVersion");
auto audio = parseEnumValues(content, "AudioVersion");
s_vAvatarRemoveAttachments = static_cast<uint8_t>(avatar["RemoveAttachments"]);
s_vAvatarTraitsAck = static_cast<uint8_t>(avatar["AvatarTraitsAck"]);
s_vAvatarQueryConical = static_cast<uint8_t>(avatarQuery["ConicalFrustums"]);
// Entity LAST_PACKET_TYPE is number of entries - 1 before NUM_PACKET_TYPE
// If parsing map failed to give LAST_PACKET_TYPE, derive from count of entries before NUM_PACKET_TYPE label.
int entityCount = 0;
{
// Count entries until NUM_PACKET_TYPE in the EntityVersion enum body
auto ep = content.find("enum class EntityVersion");
if (ep != std::string::npos) {
auto eb = content.find('{', ep);
auto ee = content.find("NUM_PACKET_TYPE", eb);
if (eb != std::string::npos && ee != std::string::npos) {
std::string body = content.substr(eb + 1, ee - eb - 1);
std::istringstream ls(body);
std::string l;
while (std::getline(ls, l)) {
auto cpos = l.find("//"); if (cpos != std::string::npos) l = l.substr(0, cpos);
auto notspace = [](int ch){ return !std::isspace(ch); };
l.erase(l.begin(), std::find_if(l.begin(), l.end(), notspace));
l.erase(std::find_if(l.rbegin(), l.rend(), notspace).base(), l.end());
if (l.empty()) continue;
if (l.find(',') != std::string::npos) entityCount++;
}
}
}
}
s_vEntityLastPacket = entityCount > 0 ? static_cast<uint8_t>(entityCount - 1) : 23;
s_vEntityParticleSpin = static_cast<uint8_t>(entity["ParticleSpin"]);
s_vAssetBakingTextureMeta = static_cast<uint8_t>(asset["BakingTextureMeta"]);
s_vEntityScriptClientCallable = static_cast<uint8_t>(entScript["ClientCallable"]);
s_vEntityQueryCbor = static_cast<uint8_t>(entQuery["CborData"]);
s_vDomainServerAddedNodeSocketTypes = static_cast<uint8_t>(domAdded["SocketTypes"]);
s_vDomainListSocketTypes = static_cast<uint8_t>(domList["SocketTypes"]);
s_vDomainListRequestSocketTypes = static_cast<uint8_t>(domListReq["SocketTypes"]);
s_vDomainConnectionDeniedExtraInfo = static_cast<uint8_t>(domDenied["IncludesExtraInfo"]);
s_vPingIncludeConnID = static_cast<uint8_t>(ping["IncludeConnectionID"]);
s_vIcePingSendPeerID = static_cast<uint8_t>(icePing["SendICEPeerID"]);
s_vAudioStopInjectors = static_cast<uint8_t>(audio["StopInjectors"]);
s_numPacketTypes = parsePacketTypeCount(content);
inited = true;
} else {
// Fallback values (best-known)
s_vAvatarRemoveAttachments = 38; // conservative guess
s_vAvatarTraitsAck = 43; // guess
s_vEntityLastPacket = 99; // guess
s_vAssetBakingTextureMeta = 22;
s_vEntityScriptClientCallable = 19;
s_vEntityQueryCbor = 24;
s_vDomainServerAddedNodeSocketTypes = 19;
s_vDomainListSocketTypes = 25;
s_vDomainListRequestSocketTypes = 23;
s_vDomainConnectionDeniedExtraInfo = 19;
s_vPingIncludeConnID = 18;
s_vIcePingSendPeerID = 18;
s_vAudioStopInjectors = 24;
s_numPacketTypes = 106;
inited = true;
}
}
vAvatarRemoveAttachments = s_vAvatarRemoveAttachments;
vAvatarTraitsAck = s_vAvatarTraitsAck;
vEntityLastPacket = s_vEntityLastPacket;
vEntityParticleSpin = s_vEntityParticleSpin;
vAssetBakingTextureMeta = s_vAssetBakingTextureMeta;
vEntityScriptClientCallable = s_vEntityScriptClientCallable;
vEntityQueryCbor = s_vEntityQueryCbor;
vAvatarQueryConical = s_vAvatarQueryConical;
vDomainServerAddedNodeSocketTypes = s_vDomainServerAddedNodeSocketTypes;
vDomainListSocketTypes = s_vDomainListSocketTypes;
vDomainListRequestSocketTypes = s_vDomainListRequestSocketTypes;
vDomainConnectionDeniedExtraInfo = s_vDomainConnectionDeniedExtraInfo;
vPingIncludeConnID = s_vPingIncludeConnID;
vIcePingSendPeerID = s_vIcePingSendPeerID;
vAudioStopInjectors = s_vAudioStopInjectors;
numPacketTypes = s_numPacketTypes;
}
uint8_t NLPacket::versionForPacketType(PacketType type) {
uint8_t vAvatarRemoveAttachments, vAvatarTraitsAck, vEntityLastPacket,
vEntityParticleSpin, vAssetBakingTextureMeta, vEntityScriptClientCallable, vEntityQueryCbor,
vAvatarQueryConical, vDomainServerAddedNodeSocketTypes, vDomainListSocketTypes, vDomainListRequestSocketTypes,
vDomainConnectionDeniedExtraInfo, vPingIncludeConnID, vIcePingSendPeerID, vAudioStopInjectors;
int numPacketTypes = 106;
ensureVersionTable(vAvatarRemoveAttachments, vAvatarTraitsAck, vEntityLastPacket,
vEntityParticleSpin, vAssetBakingTextureMeta, vEntityScriptClientCallable, vEntityQueryCbor,
vAvatarQueryConical, vDomainServerAddedNodeSocketTypes, vDomainListSocketTypes, vDomainListRequestSocketTypes,
vDomainConnectionDeniedExtraInfo, vPingIncludeConnID, vIcePingSendPeerID, vAudioStopInjectors,
numPacketTypes);
// Based on Overte's PacketHeaders.cpp versionForPacketType()
// Returns the protocol version for each packet type
switch (type) {
case PacketType::DomainConnectRequest:
return PacketVersions::DomainConnectRequest_SocketTypes;
case PacketType::DomainListRequest:
return vDomainListRequestSocketTypes;
case PacketType::DomainList:
return vDomainListSocketTypes;
case PacketType::Ping:
return vPingIncludeConnID;
case PacketType::DomainConnectionDenied:
return vDomainConnectionDeniedExtraInfo;
case PacketType::DomainConnectRequestPending:
return 17;
case PacketType::PingReply:
return 17;
case PacketType::ICEServerPeerInformation:
case PacketType::ICEServerQuery:
return 17;
case PacketType::ICEServerHeartbeat:
return 18; // ICE server heartbeat signing
case PacketType::ICEServerHeartbeatACK:
return 17;
case PacketType::ICEServerHeartbeatDenied:
return 17;
case PacketType::ICEPing:
return vIcePingSendPeerID;
case PacketType::ICEPingReply:
return 17;
case PacketType::NodeIgnoreRequest:
return 18;
case PacketType::DomainServerAddedNode:
return vDomainServerAddedNodeSocketTypes;
case PacketType::EntityAdd:
case PacketType::EntityClone:
case PacketType::EntityEdit:
case PacketType::EntityData:
case PacketType::EntityPhysics:
return vEntityLastPacket;
case PacketType::EntityQuery:
return vEntityQueryCbor;
case PacketType::EntityQueryInitialResultsComplete:
return vEntityParticleSpin;
case PacketType::AvatarQuery:
return vAvatarQueryConical;
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return vAvatarRemoveAttachments;
case PacketType::BulkAvatarTraitsAck:
case PacketType::BulkAvatarTraits:
return vAvatarTraitsAck;
case PacketType::MessagesData:
return 18; // TextOrBinaryData
case PacketType::AssetMappingOperation:
case PacketType::AssetMappingOperationReply:
case PacketType::AssetGetInfo:
case PacketType::AssetGet:
case PacketType::AssetUpload:
return vAssetBakingTextureMeta;
case PacketType::EntityScriptCallMethod:
return vEntityScriptClientCallable;
case PacketType::DomainSettings:
return 18;
case PacketType::MixedAudio:
case PacketType::SilentAudioFrame:
case PacketType::InjectAudio:
case PacketType::MicrophoneAudioNoEcho:
case PacketType::MicrophoneAudioWithEcho:
case PacketType::AudioStreamStats:
case PacketType::StopInjector:
return vAudioStopInjectors;
// For other packet types, return a default version
// In real Overte, each has a specific version
default:
return 22; // Default version for unspecified packets (matches Overte PacketHeaders.cpp)
}
}
std::vector<uint8_t> NLPacket::computeProtocolVersionSignature() {
// Compute MD5 hash of all packet type versions
// This matches Overte's ensureProtocolVersionsSignature() function
std::vector<uint8_t> buffer;
// Write number of packet types (256 max, but we'll use actual count)
uint8_t dummyA=0,dummyB=0,dummyC=0,dummyD=0,dummyE=0,dummyF=0,dummyG=0,dummyH=0,dummyI=0,dummyJ=0,dummyK=0,dummyL=0,dummyM=0,dummyN=0,dummyO=0;
int numPacketTypesInt = 106;
ensureVersionTable(dummyA, dummyB, dummyC, dummyD, dummyE,
dummyF, dummyG, dummyH, dummyI, dummyJ,
dummyK, dummyL, dummyM, dummyN, dummyO, numPacketTypesInt);
uint8_t numPacketTypes = static_cast<uint8_t>(numPacketTypesInt);
buffer.push_back(numPacketTypes);
// Write version for each packet type
for (uint8_t i = 0; i < numPacketTypes; i++) {
PacketType type = static_cast<PacketType>(i);
uint8_t version = versionForPacketType(type);
buffer.push_back(version);
}
// Debug: print buffer being hashed
static bool printed = false;
if (!printed) {
std::cout << "[OverteClient] Protocol signature input (" << buffer.size() << " bytes): ";
for (size_t i = 0; i < std::min(size_t(20), buffer.size()); i++) {
printf("%02x ", buffer[i]);
}
if (buffer.size() > 20) std::cout << "...";
std::cout << std::endl;
printed = true;
}
// Compute MD5 hash
std::vector<uint8_t> hash(MD5_DIGEST_LENGTH);
MD5(buffer.data(), buffer.size(), hash.data());
return hash;
}
} // namespace Overte

191
src/NLPacketCodec.hpp Normal file
View File

@@ -0,0 +1,191 @@
// NLPacketCodec.hpp
// Minimal NLPacket protocol implementation for Overte domain communication
// Based on Overte's NLPacket.h specification
#pragma once
#include <cstdint>
#include <vector>
#include <cstring>
#include <string>
namespace Overte {
// Packet types from Overte protocol
enum class PacketType : uint8_t {
Unknown,
DomainConnectRequestPending,
DomainList,
Ping,
PingReply,
KillAvatar,
AvatarData,
InjectAudio,
MixedAudio,
MicrophoneAudioNoEcho,
MicrophoneAudioWithEcho,
BulkAvatarData,
SilentAudioFrame,
DomainListRequest,
RequestAssignment,
CreateAssignment,
DomainConnectionDenied,
MuteEnvironment,
AudioStreamStats,
DomainServerPathQuery,
DomainServerPathResponse,
DomainServerAddedNode,
ICEServerPeerInformation,
ICEServerQuery,
OctreeStats,
SetAvatarTraits,
InjectorGainSet,
AssignmentClientStatus,
NoisyMute,
AvatarIdentity,
NodeIgnoreRequest,
DomainConnectRequest,
DomainServerRequireDTLS,
NodeJsonStats,
OctreeDataNack,
StopNode,
AudioEnvironment,
EntityEditNack,
ICEServerHeartbeat,
ICEPing,
ICEPingReply,
EntityData,
EntityQuery,
EntityAdd,
EntityErase,
EntityEdit,
DomainServerConnectionToken,
DomainSettingsRequest,
DomainSettings,
AssetGet,
AssetGetReply,
AssetUpload,
AssetUploadReply,
AssetGetInfo,
AssetGetInfoReply,
DomainDisconnectRequest,
DomainServerRemovedNode,
MessagesData,
MessagesSubscribe,
MessagesUnsubscribe,
ICEServerHeartbeatDenied,
AssetMappingOperation,
AssetMappingOperationReply,
ICEServerHeartbeatACK,
NegotiateAudioFormat,
SelectedAudioFormat,
MoreEntityShapes,
NodeKickRequest,
NodeMuteRequest,
RadiusIgnoreRequest,
UsernameFromIDRequest,
UsernameFromIDReply,
AvatarQuery,
RequestsDomainListData,
PerAvatarGainSet,
EntityScriptGetStatus,
EntityScriptGetStatusReply,
ReloadEntityServerScript,
EntityPhysics,
EntityServerScriptLog,
AdjustAvatarSorting,
OctreeFileReplacement,
CollisionEventChanges,
ReplicatedMicrophoneAudioNoEcho,
ReplicatedMicrophoneAudioWithEcho,
ReplicatedInjectAudio,
ReplicatedSilentAudioFrame,
ReplicatedAvatarIdentity,
ReplicatedKillAvatar,
ReplicatedBulkAvatarData,
DomainContentReplacementFromUrl,
DropOnNextProtocolChange_1,
EntityScriptCallMethod,
DropOnNextProtocolChange_2,
DropOnNextProtocolChange_3,
OctreeDataFileRequest,
OctreeDataFileReply,
OctreeDataPersist,
EntityClone,
EntityQueryInitialResultsComplete,
BulkAvatarTraits,
AudioSoloRequest,
BulkAvatarTraitsAck,
StopInjector,
AvatarZonePresence,
WebRTCSignaling,
NUM_PACKET_TYPE
};
using PacketVersion = uint8_t;
using LocalID = uint16_t;
using SequenceNumber = uint32_t;
// Packet version constants (from Overte source)
namespace PacketVersions {
constexpr PacketVersion DomainConnectRequest_SocketTypes = 27; // NoHostname(17) + 10
constexpr PacketVersion DomainListRequest_SocketTypes = 23; // PreSocketTypes(22) + 1
constexpr PacketVersion DomainList_SocketTypes = 25; // PrePermissionsGrid(18) + 7
constexpr PacketVersion Ping_IncludeConnectionID = 18;
}
// NLPacket structure (minimal implementation)
class NLPacket {
public:
// Packet header components
struct Header {
uint32_t sequenceAndFlags; // Sequence number (29 bits) + flags (3 bits)
PacketType type;
PacketVersion version;
LocalID sourceID; // Only for sourced packets
};
static constexpr LocalID NULL_LOCAL_ID = 0;
static constexpr size_t BASE_HEADER_SIZE = sizeof(uint32_t) + sizeof(PacketType) + sizeof(PacketVersion);
static constexpr size_t SOURCED_HEADER_SIZE = BASE_HEADER_SIZE + sizeof(LocalID);
NLPacket(PacketType type, PacketVersion version = 0, bool isReliable = false);
// Write data to packet payload
void write(const void* data, size_t size);
void writeUInt8(uint8_t value);
void writeUInt16(uint16_t value);
void writeUInt32(uint32_t value);
void writeUInt64(uint64_t value);
void writeString(const std::string& str, bool nullTerminated = true);
// Get packet data for sending
const std::vector<uint8_t>& getData() const { return m_data; }
size_t getSize() const { return m_data.size(); }
// Parse received packet
static bool parseHeader(const uint8_t* data, size_t size, Header& header);
static PacketType getType(const uint8_t* data, size_t size);
void setSequenceNumber(SequenceNumber seq);
void setSourceID(LocalID id);
// Protocol version signature
static std::vector<uint8_t> computeProtocolVersionSignature();
static uint8_t versionForPacketType(PacketType type);
private:
void writeHeader();
PacketType m_type;
PacketVersion m_version;
SequenceNumber m_sequenceNumber{0};
LocalID m_sourceID{NULL_LOCAL_ID};
bool m_isReliable;
bool m_isSourced{false};
std::vector<uint8_t> m_data;
size_t m_headerSize;
};
} // namespace Overte

View File

@@ -1,4 +1,5 @@
#include "OverteClient.hpp"
#include "NLPacketCodec.hpp"
#include <chrono>
#include <cmath>
@@ -14,8 +15,89 @@
#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() {
@@ -39,14 +121,10 @@ bool OverteClient::connect() {
// Check for authentication credentials from environment
const char* usernameEnv = std::getenv("OVERTE_USERNAME");
const char* passwordEnv = std::getenv("OVERTE_PASSWORD");
if (usernameEnv) m_username = usernameEnv;
if (passwordEnv) m_password = passwordEnv;
if (!m_username.empty()) {
std::cout << "[OverteClient] Using authentication: username=" << m_username << std::endl;
} else {
std::cout << "[OverteClient] No authentication credentials provided (set OVERTE_USERNAME/OVERTE_PASSWORD)" << std::endl;
std::cout << "[OverteClient] Username present (signature auth not yet implemented)" << std::endl;
}
// Parse ws://host:port
@@ -263,42 +341,45 @@ void OverteClient::parseNetworkPackets() {
}
void OverteClient::parseDomainPacket(const char* data, size_t len) {
if (len < 1) return;
if (len < 6) return; // NLPacket header is minimum 6 bytes
unsigned char packetType = static_cast<unsigned char>(data[0]);
// 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;
}
// Domain-level packet types
const unsigned char PACKET_TYPE_PING = 0x01;
const unsigned char PACKET_TYPE_PING_REPLY = 0x02;
const unsigned char PACKET_TYPE_DOMAIN_LIST = 0x03;
const unsigned char PACKET_TYPE_DOMAIN_CONNECTION_DENIED = 0x06;
const unsigned char PACKET_TYPE_DOMAIN_SERVER_REQUIRE_DT = 0x07;
const unsigned char PACKET_TYPE_DOMAIN_CONNECT_ACK = 0x0A;
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;
std::cout << "[OverteClient] Domain packet type: 0x" << std::hex << (int)packetType << std::dec << 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 PACKET_TYPE_DOMAIN_LIST:
handleDomainListReply(data + 1, len - 1);
case PacketType::DomainList:
handleDomainListReply(payload, payloadLen);
break;
case PACKET_TYPE_DOMAIN_CONNECTION_DENIED:
handleDomainConnectionDenied(data + 1, len - 1);
case PacketType::DomainConnectionDenied:
handleDomainConnectionDenied(payload, payloadLen);
break;
case PACKET_TYPE_DOMAIN_CONNECT_ACK:
std::cout << "[OverteClient] Domain connection acknowledged!" << std::endl;
m_domainConnected = true;
// Request domain list after successful connection
sendDomainListRequest();
case PacketType::DomainServerRequireDTLS:
std::cout << "[OverteClient] Domain server requires DTLS (not yet implemented)" << std::endl;
break;
case PACKET_TYPE_PING_REPLY:
case PacketType::PingReply:
// Keep-alive ping reply
std::cout << "[OverteClient] Ping reply received" << std::endl;
break;
default:
std::cout << "[OverteClient] Unknown domain packet type: 0x" << std::hex << (int)packetType << std::dec << std::endl;
std::cout << "[OverteClient] Unknown domain packet type: " << static_cast<int>(packetType) << std::endl;
break;
}
}
@@ -495,57 +576,115 @@ void OverteClient::handleDomainConnectionDenied(const char* data, size_t len) {
void OverteClient::sendDomainConnectRequest() {
if (!m_udpReady || m_udpFd == -1) return;
// DomainConnectRequest packet (PacketType 0x04)
const unsigned char PACKET_TYPE_DOMAIN_CONNECT_REQUEST = 0x04;
// Create NLPacket with DomainConnectRequest type and correct version
NLPacket packet(PacketType::DomainConnectRequest, PacketVersions::DomainConnectRequest_SocketTypes, true);
packet.setSequenceNumber(m_sequenceNumber++);
// Build packet with authentication
// Format: [PacketType][ProtocolVersion:string][HardwareAddress:string][MachineFingerprint:string][Username:string][Password:string]
std::string protocolVersion = "Starworld-0.1";
std::string hardwareAddress = m_sessionUUID;
std::string machineFingerprint = m_sessionUUID; // Use session UUID as fingerprint
// Build payload using Qt wire format (match Overte's NodeList.cpp structure exactly)
QtStream qs;
std::vector<char> packet;
packet.push_back(static_cast<char>(PACKET_TYPE_DOMAIN_CONNECT_REQUEST));
// 1. UUID
qs.writeQUuidFromString(m_sessionUUID);
// Helper lambda to add length-prefixed string
auto addString = [&packet](const std::string& str) {
if (str.size() > 255) {
packet.push_back(static_cast<char>(255));
packet.insert(packet.end(), str.begin(), str.begin() + 255);
} else {
packet.push_back(static_cast<char>(str.size()));
packet.insert(packet.end(), str.begin(), str.end());
// 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);
// Add protocol version
addString(protocolVersion);
// 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);
// Add hardware address (session UUID)
addString(hardwareAddress);
// 13. Place name (QString) - empty
qs.writeQString("");
// Append payload to packet
if (!qs.buf.empty()) packet.write(qs.buf.data(), qs.buf.size());
// Add machine fingerprint
addString(machineFingerprint);
// Add username (empty if not authenticated)
addString(m_username);
// Add password/token (empty if not authenticated)
addString(m_password);
ssize_t s = ::sendto(m_udpFd, packet.data(), packet.size(), 0,
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] Domain connect request sent (" << s << " bytes)" << std::endl;
std::cout << "[OverteClient] Protocol: " << protocolVersion << std::endl;
std::cout << "[OverteClient] Session: " << hardwareAddress << std::endl;
if (!m_username.empty()) {
std::cout << "[OverteClient] Username: " << m_username << std::endl;
}
// Hex dump first 32 bytes
std::cout << "[OverteClient] >>> Hex: ";
for (size_t i = 0; i < std::min(size_t(32), packet.size()); ++i) {
printf("%02x ", (unsigned char)packet[i]);
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 {
@@ -554,37 +693,40 @@ void OverteClient::sendDomainConnectRequest() {
}
void OverteClient::sendDomainListRequest() {
// Send DomainList request packet (PacketType 0x02)
// Send DomainList request packet using NLPacket format
if (!m_udpReady || m_udpFd == -1) return;
const unsigned char PACKET_TYPE_DOMAIN_LIST_REQUEST = 0x02;
char packet[1] = { static_cast<char>(PACKET_TYPE_DOMAIN_LIST_REQUEST) };
// Create NLPacket with DomainListRequest type and correct version
NLPacket packet(PacketType::DomainListRequest, PacketVersions::DomainListRequest_SocketTypes, true);
packet.setSequenceNumber(m_sequenceNumber++);
ssize_t s = ::sendto(m_udpFd, packet, sizeof(packet), 0,
// 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] DomainList request sent" << std::endl;
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) {
const unsigned char PACKET_TYPE_PING = 0x01;
// Ping packet: [PacketType][Timestamp:u64][PingType:u8]
char packet[1 + 8 + 1];
packet[0] = static_cast<char>(PACKET_TYPE_PING);
// 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();
std::memcpy(packet + 1, &micros, 8);
packet.writeUInt64(micros);
// Ping type (0 = local, 1 = public)
packet[9] = 0;
packet.writeUInt8(0);
ssize_t s = ::sendto(fd, packet, sizeof(packet), 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;

View File

@@ -67,8 +67,8 @@ private:
bool m_useSimulation{false};
bool m_domainConnected{false};
std::string m_sessionUUID; // Our client session UUID
std::string m_username; // Domain authentication username
std::string m_password; // Domain authentication password (or token)
std::string m_username; // Domain account username (for future signature-based auth)
std::uint32_t m_sequenceNumber{0}; // Packet sequence number for NLPacket protocol
// Very small in-process world state for testing
std::unordered_map<std::uint64_t, OverteEntity> m_entities;

View File

@@ -0,0 +1,65 @@
// OverteNetworkClient.hpp
#pragma once
#ifdef HAVE_OVERTE_NETWORKING
#include <memory>
#include <functional>
#include <string>
#include <QObject>
#include <QSharedPointer>
// Forward declarations to avoid including full Overte headers here
class DomainHandler;
class NodeList;
class Node;
class NLPacket;
namespace OverteNetworking {
class NetworkClient : public QObject {
Q_OBJECT
public:
NetworkClient();
~NetworkClient();
// Connect to domain server
bool connectToDomain(const QString& domainURL,
const QString& username = QString(),
const QString& password = QString());
// Disconnect from domain
void disconnect();
// Check if connected
bool isConnected() const;
// Process network events (call from main loop)
void processEvents();
// Callback for when entities are received
using EntityCallback = std::function<void(const QByteArray& entityData)>;
void setEntityCallback(EntityCallback callback);
private slots:
void onDomainChanged(const QUrl& domainURL);
void onDomainConnectionRefused(const QString& reasonMessage, int reasonCode);
void onConnectedToDomain(const QString& hostname);
void onNodeAdded(SharedNodePointer node);
void onNodeActivated(SharedNodePointer node);
private:
void setupDomainHandler();
void setupNodeList();
void handleEntityServerPacket(QSharedPointer<ReceivedMessage> message);
std::unique_ptr<DomainHandler> m_domainHandler;
QSharedPointer<NodeList> m_nodeList;
EntityCallback m_entityCallback;
bool m_connected{false};
};
} // namespace OverteNetworking
#endif // HAVE_OVERTE_NETWORKING

View File

@@ -3,6 +3,7 @@
#include "OverteClient.hpp"
#include "SceneSync.Hpp"
#include "InputHandler.hpp"
#include "DomainDiscovery.hpp"
#include <iostream>
#include <thread>
@@ -26,14 +27,48 @@ int main(int argc, char** argv) {
// Overte localhost default assumption (can override via OVERTE_URL env or --overte=ws://host:port)
std::string overteUrl = "ws://127.0.0.1:40102";
bool useDiscovery = false;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
const std::string ov = "--overte=";
const std::string disc = "--discover";
if (arg.rfind(ov, 0) == 0) overteUrl = arg.substr(ov.size());
else if (arg == disc) useDiscovery = true;
}
if (const char* envOv = std::getenv("OVERTE_URL")) {
overteUrl = envOv;
}
if (const char* envDisc = std::getenv("OVERTE_DISCOVER")) {
if (std::string(envDisc) == "1" || std::string(envDisc) == "true") useDiscovery = true;
}
if (useDiscovery) {
std::cout << "[Discovery] Querying metaverse directories for public domains..." << std::endl;
auto domains = discoverDomains(25);
if (domains.empty()) {
std::cout << "[Discovery] No domains found via directory; using provided URL: " << overteUrl << std::endl;
} else {
std::cout << "[Discovery] Found " << domains.size() << " candidate domain(s):" << std::endl;
int idx = 0;
for (const auto& d : domains) {
std::cout << " [" << idx++ << "] "
<< (d.name.empty() ? d.networkHost : d.name)
<< " -> ws://" << d.networkHost << ":" << d.httpPort
<< " (udp:" << d.udpPort << ")" << std::endl;
}
// Choose first candidate by default; allow override index via env
int choice = 0;
if (const char* envIdx = std::getenv("OVERTE_DISCOVER_INDEX")) {
try { choice = std::stoi(envIdx); } catch (...) {}
if (choice < 0 || choice >= (int)domains.size()) choice = 0;
}
const auto& pick = domains[choice];
overteUrl = std::string("ws://") + pick.networkHost + ":" + std::to_string(pick.httpPort);
// Pass UDP override via env for this process lifetime
setenv("OVERTE_UDP_PORT", std::to_string(pick.udpPort).c_str(), 1);
std::cout << "[Discovery] Selected: " << overteUrl << std::endl;
}
}
OverteClient overte(overteUrl);
// Overte is optional; warn if unreachable but continue in offline mode.
if (!overte.connect()) {