Compare commits
57 Commits
ebbbffe2e3
...
38cb81560c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38cb81560c | ||
|
|
f533e5da63 | ||
|
|
2f900c775e | ||
|
|
31f235dbce | ||
|
|
a489d6f613 | ||
|
|
eaf56daac2 | ||
|
|
acb9747e4a | ||
|
|
dd04d483ef | ||
|
|
8c4cfb5261 | ||
|
|
3cd660e7e1 | ||
|
|
3e657c1e02 | ||
|
|
765d63bbb5 | ||
|
|
b10ee9f434 | ||
|
|
a976107332 | ||
|
|
c1b109d95a | ||
|
|
47e427cc26 | ||
|
|
b0c428bb66 | ||
|
|
a391c1814e | ||
|
|
a0e2231137 | ||
|
|
8c541ed1c5 | ||
|
|
c65fe9e278 | ||
|
|
19cac1dbd8 | ||
|
|
f7e75bb706 | ||
|
|
313acd4d69 | ||
|
|
3a35ac86da | ||
|
|
27c13044df | ||
|
|
0659f6fdb7 | ||
|
|
e29ef3a051 | ||
|
|
043a9e1798 | ||
|
|
5b242fdea6 | ||
|
|
69b388e4fe | ||
|
|
25441fcc1f | ||
|
|
cd58444bec | ||
|
|
2adef096eb | ||
|
|
01bc7a2519 | ||
|
|
2050c9b478 | ||
|
|
b42df944c1 | ||
|
|
35034d5de6 | ||
|
|
182a5c23de | ||
|
|
aab3d069f6 | ||
|
|
6685bf1579 | ||
|
|
a61e2bb7c4 | ||
|
|
332018952d | ||
|
|
ea6caa60d6 | ||
|
|
d80df50030 | ||
|
|
6ced91125a | ||
|
|
03d2c965b6 | ||
|
|
e54dea19a9 | ||
|
|
336f1b25b7 | ||
|
|
2a50d7a85b | ||
|
|
747f1ded6a | ||
|
|
a8c76649b6 | ||
|
|
65d25a1bca | ||
|
|
c463b8ea40 | ||
|
|
9b3f43a605 | ||
|
|
4150738be4 | ||
|
|
e5e79c7b4a |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
148
src/DomainDiscovery.cpp
Normal 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
15
src/DomainDiscovery.hpp
Normal 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
497
src/NLPacketCodec.cpp
Normal 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
191
src/NLPacketCodec.hpp
Normal 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
|
||||
@@ -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, µs, 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
65
src/OverteNetworkClient.hpp
Normal file
65
src/OverteNetworkClient.hpp
Normal 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
|
||||
35
src/main.cpp
35
src/main.cpp
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user