Compare commits

...

28 Commits

Author SHA1 Message Date
MayaTheShy
c35349b891 feat: implement parsing of DomainList packets to discover mixer endpoints and update EntityServer connection 2025-11-08 16:58:31 -05:00
MayaTheShy
04e3257e12 feat: enhance entity packet parsing with additional packet types and improved logging 2025-11-08 16:58:24 -05:00
MayaTheShy
30a20c9b30 fix: ensure all test entities are cleaned up before completion in inject_test_entities.py 2025-11-08 16:58:17 -05:00
MayaTheShy
6974ab8ee2 feat: implement UDP socket creation and binding for EntityServer in connectEntityServer 2025-11-08 16:39:36 -05:00
MayaTheShy
095140383a chore: update file permissions for inject_test_entities.py 2025-11-08 16:39:28 -05:00
MayaTheShy
ad4dc7b0df feat: add script to send test entity packets to OverteClient 2025-11-08 16:33:02 -05:00
MayaTheShy
1917dbb29c feat: add simulation mode logging in OverteClient connection 2025-11-08 16:10:39 -05:00
MayaTheShy
0398af67cf feat: add logging for received UDP packets in OverteClient 2025-11-08 16:10:04 -05:00
MayaTheShy
5c00af0c05 feat: enhance packet handling in OverteClient by parsing entity server packets 2025-11-08 16:05:32 -05:00
MayaTheShy
f5dee50cdb feat: implement entity packet parsing and handling in OverteClient 2025-11-08 16:05:12 -05:00
MayaTheShy
0325222354 feat: implement connection to EntityServer with UDP socket setup 2025-11-08 16:04:44 -05:00
MayaTheShy
28ec707d30 feat: add entity server connection handling in OverteClient 2025-11-08 16:04:35 -05:00
MayaTheShy
047edca7de refactor: remove redundant implementation of removeNode function in StardustBridge 2025-11-08 16:01:57 -05:00
MayaTheShy
f400c9272e feat: implement removeNode function to delete nodes from StardustBridge 2025-11-08 16:01:05 -05:00
MayaTheShy
e17b57b1e2 fix: remove duplicate assignment of m_fnCreateNode in loadBridge function 2025-11-08 16:01:00 -05:00
MayaTheShy
b091354f29 feat: integrate simulation mode for entity seeding and transform updates in OverteClient 2025-11-08 16:00:53 -05:00
MayaTheShy
4c2026c3ba feat: enhance SceneSync update method to process entity deletions after updates 2025-11-08 16:00:27 -05:00
MayaTheShy
f4866dd8fe feat: update OverteClient to enhance entity management and clarify documentation 2025-11-08 16:00:21 -05:00
MayaTheShy
ba6ff968db feat: add removeNode function to StardustBridge for node deletion 2025-11-08 16:00:11 -05:00
MayaTheShy
9e955e7548 feat: implement node removal functionality in sdxr_start and add corresponding extern function 2025-11-08 15:56:18 -05:00
MayaTheShy
aa9fee158b feat: update sdxr_start to use future for event loop and improve error handling
The demo cube are now rendering and the client remain stable.
2025-11-08 15:53:13 -05:00
MayaTheShy
502e055149 feat: extend sdxr_start to handle additional root events and save state response 2025-11-08 15:48:28 -05:00
MayaTheShy
74265dd855 feat: enhance sdxr_start with retry logic for fusion client connection and improved error handling 2025-11-08 15:46:52 -05:00
MayaTheShy
70f58a6d5e feat: add stardust-xr-molecules dependency to enhance functionality 2025-11-08 15:45:24 -05:00
MayaTheShy
6ac8991bd3 feat: simplify accent color usage in sdxr_start by using direct import 2025-11-08 15:42:50 -05:00
MayaTheShy
62b1ba3db1 feat: add stardust-xr-molecules dependency for enhanced integration 2025-11-08 15:42:45 -05:00
MayaTheShy
93e9ed3a12 fix: correct accent color import in sdxr_start function 2025-11-08 15:42:02 -05:00
MayaTheShy
a1d404c0a1 feat: enhance sdxr_start with improved client connection handling and event loop management 2025-11-08 15:40:58 -05:00
9 changed files with 494 additions and 77 deletions

1
bridge/Cargo.lock generated
View File

@@ -2513,6 +2513,7 @@ dependencies = [
"serde",
"stardust-xr-asteroids",
"stardust-xr-fusion",
"stardust-xr-molecules",
"tokio",
"tracing-subscriber",
"zbus",

View File

@@ -21,6 +21,9 @@ path = "../third_party/asteroids"
[dependencies.stardust-xr-fusion]
path = "../third_party/core/fusion"
[dependencies.stardust-xr-molecules]
path = "../third_party/molecules"
[features]
# Feature enabling real StardustXR integration when crates are present.
real = []

View File

@@ -13,7 +13,11 @@ use stardust_xr_asteroids::{
elements::{PlaySpace, Lines},
Migrate, Reify,
};
use stardust_xr_asteroids::{CustomElement, Transformable};
use stardust_xr_asteroids::{CustomElement, Transformable, Projector, Context};
use stardust_xr_molecules::accent_color::AccentColor;
use stardust_xr_fusion::objects::connect_client as fusion_connect_client;
use stardust_xr_fusion::node::NodeType;
use stardust_xr_fusion::root::RootAspect;
use tokio::runtime::Runtime;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
@@ -30,6 +34,7 @@ impl Default for BridgeState {
enum Command {
Create { c_id: u64, name: String, transform: Mat4 },
Update { c_id: u64, transform: Mat4 },
Remove { c_id: u64 },
Shutdown,
}
@@ -61,13 +66,15 @@ impl Reify for BridgeState {
let vis_scale = glam::Vec3::splat(0.20) * scale.x;
// Build cube edges as 12 line segments
use stardust_xr_molecules::lines::{line_from_points, LineExt};
use stardust_xr_fusion::values::color::rgba_linear;
use stardust_xr_fusion::drawable::{Line, LinePoint};
use stardust_xr_fusion::values::{color::rgba_linear, Vector3};
let t = 0.004; // thickness
let c = rgba_linear!(0.8, 0.8, 0.9, 1.0);
let hs = 0.5f32; // half size in model space (unit cube)
let mut seg = |a: [f32;3], b: [f32;3]| {
line_from_points(vec![a, b]).thickness(t).color(c)
let mut seg = |a: [f32;3], b: [f32;3]| -> Line {
let p0 = LinePoint { point: Vector3 { x: a[0], y: a[1], z: a[2] }, thickness: t, color: c };
let p1 = LinePoint { point: Vector3 { x: b[0], y: b[1], z: b[2] }, thickness: t, color: c };
Line { points: vec![p0, p1], cyclic: false }
};
let corners = [
[-hs, -hs, -hs], [ hs, -hs, -hs], [ hs, hs, -hs], [-hs, hs, -hs],
@@ -96,7 +103,7 @@ impl Reify for BridgeState {
}
static STARTED: AtomicBool = AtomicBool::new(false);
static CONNECTED: AtomicBool = AtomicBool::new(false);
static STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
lazy_static::lazy_static! {
static ref CTRL: Mutex<Ctrl> = Mutex::new(Ctrl::default());
}
@@ -151,12 +158,6 @@ pub extern "C" fn sdxr_start(app_id: *const std::os::raw::c_char) -> i32 {
.build()
.expect("tokio runtime");
let handle = std::thread::spawn(move || {
// Initialize tracing for debugging
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()
.add_directive("stardust_xr_fusion=debug".parse().unwrap()))
.try_init();
let res = rt.block_on(async move {
// Spawn command processor task that updates shared state
let cmd_task = tokio::spawn(async move {
@@ -179,45 +180,78 @@ pub extern "C" fn sdxr_start(app_id: *const std::os::raw::c_char) -> i32 {
}
}
}
Command::Shutdown => break,
Command::Remove { c_id } => {
if let Ok(mut state) = shared_for_commands.lock() {
if state.nodes.remove(&c_id).is_some() {
println!("[bridge] remove node id={} (remaining={})", c_id, state.nodes.len());
}
}
}
Command::Shutdown => { STOP_REQUESTED.store(true, Ordering::SeqCst); break; }
}
}
});
println!("[bridge] Connecting to Stardust server...");
// Debug: try raw socket connection first
let socket_path = std::env::var("XDG_RUNTIME_DIR")
.map(|dir| format!("{}/stardust-0", dir))
.or_else(|_| std::env::var("STARDUST_INSTANCE").map(|inst| format!("/run/user/1000/{}", inst)))
.unwrap_or_else(|_| "/run/user/1000/stardust-0".to_string());
println!("[bridge] Socket path: {}", socket_path);
match tokio::net::UnixStream::connect(&socket_path).await {
Ok(_) => println!("[bridge] Raw socket connection OK"),
Err(e) => println!("[bridge] Raw socket connection failed: {}", e),
}
// Run the client - asteroids will manage the projector and call reify() each frame
// This blocks until the client disconnects or is shut down
match stardust_xr_fusion::client::Client::connect().await {
Ok(_client) => {
println!("[bridge] Connected to Stardust server successfully");
// Now try to run the full asteroids client
ast::client::run::<BridgeState>(&[]).await;
},
Err(e) => {
println!("[bridge] Failed to connect to Stardust server: {:?}", e);
// Retry fusion connect for a few seconds to handle compositor wake-up races.
let mut client = loop {
match stardust_xr_fusion::client::Client::connect().await {
Ok(c) => break c,
Err(e) => {
eprintln!("[bridge] Fusion connect failed: {:?}; retrying...", e);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
if STOP_REQUESTED.load(Ordering::SeqCst) { return; }
}
}
};
let dbus_connection = match fusion_connect_client().await {
Ok(c) => c,
Err(e) => {
eprintln!("[bridge] DBus connect failed: {:?}; continuing without context extras", e);
// Fallback to a new connection attempt with default
match fusion_connect_client().await {
Ok(c2) => c2,
Err(_) => return,
}
}
};
let accent_color = AccentColor::new(dbus_connection.clone());
let context = Context { dbus_connection, accent_color };
let mut state = BridgeState::default();
let mut projector = Projector::create(&state, &context, client.get_root().clone().as_spatial_ref(), "/".into());
println!("[bridge] Persistent event loop running");
let event_loop_fut = client.sync_event_loop(|client, flow| {
use stardust_xr_fusion::root::{RootEvent, ClientState as SaveStatePayload};
let mut frames = vec![];
while let Some(re) = client.get_root().recv_root_event() {
match re {
RootEvent::Ping { response } => {
let _ = response.send_ok(());
}
RootEvent::Frame { info } => frames.push(info),
RootEvent::SaveState { response } => {
let payload = SaveStatePayload { data: None, root: client.get_root().id(), spatial_anchors: Default::default() };
let _ = response.send_ok(payload);
}
}
}
if frames.is_empty() { return; }
for frame in frames {
if let Ok(ctrl) = CTRL.lock() { if let Some(shared) = &ctrl.shared_state { if let Ok(ss) = shared.lock() { state.nodes = ss.nodes.clone(); } } }
state.on_frame(&frame);
projector.frame(&context, &frame, &mut state);
}
projector.update(&context, &mut state);
if STOP_REQUESTED.load(Ordering::SeqCst) { flow.stop(); }
});
if let Err(e) = event_loop_fut.await {
eprintln!("[bridge] Event loop error: {:?}", e);
}
println!("[bridge] Client disconnected");
println!("[bridge] Event loop terminated");
let _ = cmd_task;
});
drop(rt);
let _ = res;
STARTED.store(false, Ordering::SeqCst);
CONNECTED.store(false, Ordering::SeqCst);
});
ctrl.rt = None; // runtime consumed inside thread
@@ -225,24 +259,12 @@ pub extern "C" fn sdxr_start(app_id: *const std::os::raw::c_char) -> i32 {
// Store the shared state so we can read from it later
ctrl.shared_state = Some(shared_state);
// Give the async runtime a moment to attempt connection
std::thread::sleep(std::time::Duration::from_millis(100));
// If the thread is still alive, assume connection succeeded
// (if it failed immediately, STARTED would be false)
if STARTED.load(Ordering::SeqCst) {
CONNECTED.store(true, Ordering::SeqCst);
}
STOP_REQUESTED.store(false, Ordering::SeqCst);
0
}
#[no_mangle]
pub extern "C" fn sdxr_poll() -> i32 {
if !STARTED.load(Ordering::SeqCst) { return -1; }
// Return 0 if connected and running, -1 if disconnected
if CONNECTED.load(Ordering::SeqCst) { 0 } else { -1 }
}
pub extern "C" fn sdxr_poll() -> i32 { if !STARTED.load(Ordering::SeqCst) { -1 } else { 0 } }
#[no_mangle]
pub extern "C" fn sdxr_shutdown() {
@@ -283,6 +305,14 @@ pub extern "C" fn sdxr_update_node(id: u64, mat4: *const f32) -> i32 {
0
}
#[no_mangle]
pub extern "C" fn sdxr_remove_node(id: u64) -> i32 {
if !STARTED.load(Ordering::SeqCst) { return -1; }
let ctrl = CTRL.lock().unwrap();
if let Some(tx) = &ctrl.tx { let _ = tx.send(Command::Remove { c_id: id }); }
0
}
// Optional: expose number of nodes for diagnostics
#[no_mangle]
pub extern "C" fn sdxr_node_count() -> u64 {

View File

@@ -7,6 +7,7 @@
#include <glm/gtc/matrix_transform.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
@@ -76,13 +77,20 @@ bool OverteClient::connect() {
return false;
}
// Seed a couple of demo entities.
OverteEntity a{ m_nextEntityId++, "CubeA", glm::mat4(1.0f) };
OverteEntity b{ m_nextEntityId++, "CubeB", glm::mat4(1.0f) };
m_entities.emplace(a.id, a);
m_entities.emplace(b.id, b);
m_updateQueue.push_back(a.id);
m_updateQueue.push_back(b.id);
m_useSimulation = (std::getenv("STARWORLD_SIMULATE") != nullptr);
if (m_useSimulation) {
// Seed a couple of demo entities.
OverteEntity a{ m_nextEntityId++, "CubeA", glm::mat4(1.0f) };
OverteEntity b{ m_nextEntityId++, "CubeB", glm::mat4(1.0f) };
m_entities.emplace(a.id, a);
m_entities.emplace(b.id, b);
m_updateQueue.push_back(a.id);
m_updateQueue.push_back(b.id);
std::cout << "[OverteClient] Simulation mode enabled (STARWORLD_SIMULATE=1)" << std::endl;
} else {
std::cout << "[OverteClient] Waiting for entity packets from Overte server..." << std::endl;
std::cout << "[OverteClient] Tip: Set STARWORLD_SIMULATE=1 to enable demo entities" << std::endl;
}
return true;
}
@@ -93,7 +101,44 @@ bool OverteClient::connectAvatarMixer() {
}
bool OverteClient::connectEntityServer() {
// TODO: Connect to EntityServer and subscribe to updates.
// Send DomainList request to discover EntityServer endpoint
sendDomainListRequest();
// Create UDP socket for EntityServer and BIND it to receive packets
m_entityFd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (m_entityFd == -1) {
std::cerr << "[OverteClient] Failed to create EntityServer socket: " << std::strerror(errno) << std::endl;
return false;
}
// Make non-blocking
::fcntl(m_entityFd, F_SETFL, O_NONBLOCK);
// Bind to port 40103 to receive entity packets
sockaddr_in bindAddr{};
bindAddr.sin_family = AF_INET;
bindAddr.sin_addr.s_addr = INADDR_ANY; // Listen on all interfaces
bindAddr.sin_port = htons(m_port + 1); // 40103
if (::bind(m_entityFd, reinterpret_cast<sockaddr*>(&bindAddr), sizeof(bindAddr)) == -1) {
std::cerr << "[OverteClient] Failed to bind EntityServer socket to port " << (m_port + 1)
<< ": " << std::strerror(errno) << std::endl;
::close(m_entityFd);
m_entityFd = -1;
return false;
}
// Store the target address for sending (if needed)
m_entityAddr = {};
sockaddr_in* addr = reinterpret_cast<sockaddr_in*>(&m_entityAddr);
addr->sin_family = AF_INET;
addr->sin_port = htons(m_port + 1);
::inet_pton(AF_INET, m_host.c_str(), &addr->sin_addr);
m_entityAddrLen = sizeof(sockaddr_in);
m_entityServerReady = true;
std::cout << "[OverteClient] EntityServer socket bound and listening on port " << (m_port + 1) << std::endl;
m_entityServer = true;
return true;
}
@@ -107,7 +152,7 @@ bool OverteClient::connectAudioMixer() {
void OverteClient::poll() {
if (!m_connected) return;
// Try a lightweight UDP ping if ready
// Try a lightweight UDP ping if ready (placeholder for avatar mixer handshake)
if (m_udpReady && m_udpFd != -1) {
const char ping[4] = {'P','I','N','G'};
ssize_t s = ::sendto(m_udpFd, ping, sizeof(ping), 0, reinterpret_cast<sockaddr*>(&m_udpAddr), m_udpAddrLen);
@@ -118,26 +163,227 @@ void OverteClient::poll() {
sockaddr_storage from{}; socklen_t fromlen = sizeof(from);
ssize_t r = ::recvfrom(m_udpFd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&from), &fromlen);
if (r > 0) {
std::cout << "[OverteClient] UDP packet received (" << r << " bytes)" << std::endl;
// Parse as potential domain/avatar packets
std::cout << "[OverteClient] Domain UDP packet received (" << r << " bytes, type=0x"
<< std::hex << (int)(unsigned char)buf[0] << std::dec << ")" << std::endl;
parseEntityPacket(buf, static_cast<size_t>(r));
}
}
// Simulate entity transforms changing slightly over time.
static auto t0 = std::chrono::steady_clock::now();
const float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - t0).count();
// Parse entity server packets
parseNetworkPackets();
for (auto& [id, e] : m_entities) {
const float r = 0.25f + 0.05f * static_cast<float>(id);
const float x = std::cos(t * 0.5f + static_cast<float>(id)) * r;
const float z = std::sin(t * 0.5f + static_cast<float>(id)) * r;
e.transform = glm::translate(glm::mat4(1.0f), glm::vec3{x, 1.25f, z});
m_updateQueue.push_back(id);
if (m_useSimulation) {
// Simulate entity transforms changing slightly over time.
static auto t0 = std::chrono::steady_clock::now();
const float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - t0).count();
for (auto& [id, e] : m_entities) {
const float r = 0.25f + 0.05f * static_cast<float>(id);
const float x = std::cos(t * 0.5f + static_cast<float>(id)) * r;
const float z = std::sin(t * 0.5f + static_cast<float>(id)) * r;
e.transform = glm::translate(glm::mat4(1.0f), glm::vec3{x, 1.25f, z});
m_updateQueue.push_back(id);
}
}
}
void OverteClient::parseNetworkPackets() {
// Read from EntityServer socket
if (m_entityServerReady && m_entityFd != -1) {
char buf[1500];
sockaddr_storage from{}; socklen_t fromlen = sizeof(from);
ssize_t r = ::recvfrom(m_entityFd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&from), &fromlen);
if (r > 0) {
std::cout << "[OverteClient] EntityServer packet received (" << r << " bytes, type=0x"
<< std::hex << (int)(unsigned char)buf[0] << std::dec << ")" << std::endl;
parseEntityPacket(buf, static_cast<size_t>(r));
}
}
}
void OverteClient::parseEntityPacket(const char* data, size_t len) {
// Overte packet structure (simplified):
// - Byte 0: PacketType
// - Following bytes: payload (varies by type)
if (len < 1) return;
unsigned char packetType = static_cast<unsigned char>(data[0]);
// Overte PacketType enum values (reference from protocol documentation)
// Reference: https://github.com/overte-org/overte/blob/master/libraries/networking/src/udt/PacketHeaders.h
const unsigned char PACKET_TYPE_DOMAIN_LIST = 0x03;
const unsigned char PACKET_TYPE_DOMAIN_CONNECTION_DENIED = 0x06;
const unsigned char PACKET_TYPE_PING = 0x01;
const unsigned char PACKET_TYPE_PING_REPLY = 0x02;
// Entity packet types
const unsigned char PACKET_TYPE_ENTITY_ADD = 0x10;
const unsigned char PACKET_TYPE_ENTITY_EDIT = 0x11;
const unsigned char PACKET_TYPE_ENTITY_ERASE = 0x12;
const unsigned char PACKET_TYPE_ENTITY_QUERY = 0x15;
const unsigned char PACKET_TYPE_OCTREE_STATS = 0x16;
switch (packetType) {
case PACKET_TYPE_DOMAIN_LIST:
handleDomainListReply(data + 1, len - 1);
break;
case PACKET_TYPE_PING_REPLY:
std::cout << "[OverteClient] Ping reply received from domain" << std::endl;
break;
case PACKET_TYPE_ENTITY_ADD: {
// EntityAdd packet structure (simplified):
// u64 entityID, string name, vec3 position, quat rotation, vec3 dimensions, ...
if (len < 9) break; // need at least 1+8 bytes
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
// Parse name (null-terminated string after ID)
size_t offset = 9;
std::string name;
while (offset < len && data[offset] != '\0') {
name += data[offset++];
}
if (name.empty()) name = "Entity_" + std::to_string(entityId);
// Create entity with a visible position spread out in front of user
// Position entities in a grid pattern for visibility
float spacing = 0.5f;
int index = static_cast<int>(entityId % 10);
float x = (index % 3) * spacing - spacing; // -0.5, 0, 0.5
float y = 1.5f; // Eye level
float z = -2.0f - (index / 3) * spacing; // Start 2m in front, spread back
glm::vec3 position(x, y, z);
glm::mat4 transform = glm::translate(glm::mat4(1.0f), position);
OverteEntity entity{entityId, name, transform};
m_entities[entityId] = entity;
m_updateQueue.push_back(entityId);
std::cout << "[OverteClient] Entity added: " << name << " (id=" << entityId
<< ") at pos(" << x << ", " << y << ", " << z << ")" << std::endl;
break;
}
case PACKET_TYPE_ENTITY_EDIT: {
// EntityEdit packet: u64 entityID, property flags, property data...
if (len < 9) break;
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
auto it = m_entities.find(entityId);
if (it != m_entities.end()) {
// TODO: parse property flags and update transform
// For now, mark as updated
m_updateQueue.push_back(entityId);
}
break;
}
case PACKET_TYPE_ENTITY_ERASE: {
// EntityErase packet: u64 entityID
if (len < 9) break;
std::uint64_t entityId;
std::memcpy(&entityId, data + 1, 8);
auto it = m_entities.find(entityId);
if (it != m_entities.end()) {
m_entities.erase(it);
m_deleteQueue.push_back(entityId);
std::cout << "[OverteClient] Entity erased: id=" << entityId << std::endl;
}
break;
}
default:
// Log unknown packet types for debugging
if (packetType != PACKET_TYPE_PING && packetType != PACKET_TYPE_PING_REPLY) {
std::cout << "[OverteClient] Unknown packet type: 0x" << std::hex << (int)packetType << std::dec << std::endl;
}
break;
}
}
void OverteClient::handleDomainListReply(const char* data, size_t len) {
// DomainList packet contains mixer endpoints
// Format: sequence of [NodeType:u8][UUID:16bytes][PublicSocket:sockaddr][LocalSocket:sockaddr]
std::cout << "[OverteClient] DomainList reply received (" << len << " bytes)" << std::endl;
if (len < 1) return;
// Parse number of nodes
size_t offset = 0;
while (offset + 1 < len) {
unsigned char nodeType = data[offset++];
// Skip UUID (16 bytes)
if (offset + 16 > len) break;
offset += 16;
// Read public socket address (sockaddr_in or sockaddr_in6)
if (offset + sizeof(sockaddr_in) > len) break;
sockaddr_in publicAddr;
std::memcpy(&publicAddr, data + offset, sizeof(sockaddr_in));
offset += sizeof(sockaddr_in);
// Skip local socket (same size)
if (offset + sizeof(sockaddr_in) > len) break;
offset += sizeof(sockaddr_in);
// NodeType values from Overte:
// 0 = DomainServer, 1 = EntityServer, 2 = Agent, 3 = AudioMixer, 4 = AvatarMixer, 5 = AssetServer, 6 = MessagesMixer, 7 = EntityScriptServer
const unsigned char NODE_TYPE_ENTITY_SERVER = 1;
const unsigned char NODE_TYPE_AVATAR_MIXER = 4;
const unsigned char NODE_TYPE_AUDIO_MIXER = 3;
char addrStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &publicAddr.sin_addr, addrStr, sizeof(addrStr));
int port = ntohs(publicAddr.sin_port);
std::cout << "[OverteClient] Mixer discovered: type=" << (int)nodeType
<< " addr=" << addrStr << ":" << port << std::endl;
if (nodeType == NODE_TYPE_ENTITY_SERVER && !m_entityServerReady) {
// Update EntityServer connection to use discovered address
std::cout << "[OverteClient] Using discovered EntityServer at " << addrStr << ":" << port << std::endl;
// Update target address for EntityServer
sockaddr_in* entityAddr = reinterpret_cast<sockaddr_in*>(&m_entityAddr);
entityAddr->sin_family = AF_INET;
entityAddr->sin_port = publicAddr.sin_port;
entityAddr->sin_addr = publicAddr.sin_addr;
m_entityAddrLen = sizeof(sockaddr_in);
// Send EntityQuery to request all entities
sendEntityQuery();
}
}
}
void OverteClient::sendDomainListRequest() {
// Send DomainList request packet (PacketType 0x02 typically)
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) };
ssize_t s = ::sendto(m_udpFd, packet, sizeof(packet), 0,
reinterpret_cast<sockaddr*>(&m_udpAddr), m_udpAddrLen);
if (s > 0) {
std::cout << "[OverteClient] DomainList request sent" << std::endl;
}
}
void OverteClient::sendMovementInput(const glm::vec3& linearVelocity) {
// TODO: Package and send to AvatarMixer as appropriate (e.g. MyAvatar data).
(void)linearVelocity; // silence unused warning in the stub
(void)linearVelocity; // TODO: send to avatar mixer
}
std::vector<OverteEntity> OverteClient::consumeUpdatedEntities() {
@@ -150,3 +396,9 @@ std::vector<OverteEntity> OverteClient::consumeUpdatedEntities() {
m_updateQueue.clear();
return out;
}
std::vector<std::uint64_t> OverteClient::consumeDeletedEntities() {
std::vector<std::uint64_t> out;
out.swap(m_deleteQueue); // efficient clear
return out;
}

View File

@@ -19,8 +19,9 @@ struct OverteEntity {
glm::mat4 transform{1.0f};
};
// Lightweight stub over Overte networking layer. Designed to be replaced by
// real Overte SDK calls while keeping the app testable.
// Lightweight client for Overte mixers/entities. Designed to follow Overte's
// standards. For now includes a minimal parser scaffold; simulation can be
// optionally enabled via STARWORLD_SIMULATE=1.
class OverteClient {
public:
explicit OverteClient(std::string domainUrl)
@@ -43,8 +44,14 @@ public:
// Entity accessors
const std::unordered_map<std::uint64_t, OverteEntity>& entities() const { return m_entities; }
std::vector<OverteEntity> consumeUpdatedEntities();
std::vector<std::uint64_t> consumeDeletedEntities();
private:
void parseNetworkPackets(); // standards-aligned parsing (scaffold)
void parseEntityPacket(const char* data, size_t len);
void handleDomainListReply(const char* data, size_t len);
void sendDomainListRequest();
std::string m_domainUrl;
std::string m_host{"127.0.0.1"};
int m_port{40102};
@@ -52,10 +59,12 @@ private:
bool m_avatarMixer{false};
bool m_entityServer{false};
bool m_audioMixer{false};
bool m_useSimulation{false};
// Very small in-process world state for testing
std::unordered_map<std::uint64_t, OverteEntity> m_entities;
std::vector<std::uint64_t> m_updateQueue; // ids of entities updated since last consume
std::vector<std::uint64_t> m_deleteQueue; // ids of entities to delete
std::uint64_t m_nextEntityId{1};
// Networking
@@ -63,5 +72,12 @@ private:
bool m_udpReady{false};
struct sockaddr_storage m_udpAddr{};
socklen_t m_udpAddrLen{0};
// EntityServer connection
int m_entityFd{-1};
bool m_entityServerReady{false};
sockaddr_storage m_entityAddr{};
socklen_t m_entityAddrLen{0};
std::vector<char> m_entityBuffer; // accumulate partial packets
};

View File

@@ -21,5 +21,15 @@ void SceneSync::update(StardustBridge& stardust, OverteClient& overte) {
stardust.updateNodeTransform(it->second, e.transform);
}
}
// Process deletions after updates to avoid create-then-delete thrash.
auto deleted = overte.consumeDeletedEntities();
for (auto entId : deleted) {
auto it = s_entityNodeMap.find(entId);
if (it != s_entityNodeMap.end()) {
stardust.removeNode(it->second);
s_entityNodeMap.erase(it);
}
}
}

View File

@@ -161,6 +161,16 @@ bool StardustBridge::updateNodeTransform(NodeId id, const glm::mat4& transform)
return true;
}
bool StardustBridge::removeNode(NodeId id) {
auto it = m_nodes.find(id);
if (it == m_nodes.end()) return false;
m_nodes.erase(it);
if (m_fnRemoveNode) {
(void)m_fnRemoveNode(id);
}
return true;
}
void StardustBridge::poll() {
if (!m_connected) return;
@@ -236,7 +246,9 @@ bool StardustBridge::loadBridge() {
m_fnPoll = reinterpret_cast<fn_poll_t>(req("sdxr_poll"));
m_fnShutdown = reinterpret_cast<fn_shutdown_t>(req("sdxr_shutdown"));
m_fnCreateNode = reinterpret_cast<fn_create_node_t>(req("sdxr_create_node"));
m_fnCreateNode = reinterpret_cast<fn_create_node_t>(req("sdxr_create_node"));
m_fnUpdateNode = reinterpret_cast<fn_update_node_t>(req("sdxr_update_node"));
m_fnRemoveNode = reinterpret_cast<fn_remove_node_t>(req("sdxr_remove_node"));
if (m_fnStart && m_fnPoll && m_fnCreateNode && m_fnUpdateNode) {
m_bridgeHandle = h;
std::cout << "[StardustBridge] Loaded Rust bridge: " << path << std::endl;

View File

@@ -28,6 +28,9 @@ public:
// Update a node's transform. Returns false if the node doesn't exist.
bool updateNodeTransform(NodeId id, const glm::mat4& transform);
// Remove a node. Returns false if the node doesn't exist.
bool removeNode(NodeId id);
// Poll compositor events and input. Non-blocking.
void poll();
@@ -77,11 +80,13 @@ private:
using fn_shutdown_t = void(*)();
using fn_create_node_t = std::uint64_t(*)(const char*, const float*);
using fn_update_node_t = int(*)(std::uint64_t, const float*);
using fn_remove_node_t = int(*)(std::uint64_t);
fn_start_t m_fnStart{nullptr};
fn_poll_t m_fnPoll{nullptr};
fn_shutdown_t m_fnShutdown{nullptr};
fn_create_node_t m_fnCreateNode{nullptr};
fn_update_node_t m_fnUpdateNode{nullptr};
fn_remove_node_t m_fnRemoveNode{nullptr};
bool loadBridge();
};

88
tools/inject_test_entities.py Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
Simple script to send test entity packets to the OverteClient.
This simulates what the EntityServer would send.
"""
import socket
import struct
import time
# Overte packet types (as defined in your C++ code)
PACKET_TYPE_ENTITY_ADD = 0x10
PACKET_TYPE_ENTITY_EDIT = 0x11
PACKET_TYPE_ENTITY_ERASE = 0x12
def send_entity_add(sock, addr, entity_id, name):
"""Send an EntityAdd packet"""
# Packet structure: [type:u8][id:u64][name:null-terminated string]
packet = struct.pack('<BQ', PACKET_TYPE_ENTITY_ADD, entity_id)
packet += name.encode('utf-8') + b'\x00'
sock.sendto(packet, addr)
print(f"Sent EntityAdd: id={entity_id}, name={name}")
def send_entity_edit(sock, addr, entity_id):
"""Send an EntityEdit packet (simplified)"""
# Packet structure: [type:u8][id:u64][property flags...]
packet = struct.pack('<BQ', PACKET_TYPE_ENTITY_EDIT, entity_id)
sock.sendto(packet, addr)
print(f"Sent EntityEdit: id={entity_id}")
def send_entity_erase(sock, addr, entity_id):
"""Send an EntityErase packet"""
# Packet structure: [type:u8][id:u64]
packet = struct.pack('<BQ', PACKET_TYPE_ENTITY_ERASE, entity_id)
sock.sendto(packet, addr)
print(f"Sent EntityErase: id={entity_id}")
def main():
# Target: your OverteClient's EntityServer socket
target_host = '127.0.0.1'
target_port = 40103
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
addr = (target_host, target_port)
print(f"Sending test entity packets to {target_host}:{target_port}")
print("Make sure your stardust-overte-client is running!")
print()
time.sleep(1)
# Create some test entities
send_entity_add(sock, addr, 1001, "TestCube")
time.sleep(0.5)
send_entity_add(sock, addr, 1002, "TestSphere")
time.sleep(0.5)
send_entity_add(sock, addr, 1003, "TestBox")
time.sleep(2)
# Update an entity
print("\nSending edit packets...")
for i in range(3):
send_entity_edit(sock, addr, 1001)
time.sleep(0.5)
time.sleep(2)
# Delete an entity
print("\nDeleting entity 1002...")
send_entity_erase(sock, addr, 1002)
time.sleep(1)
# Clean up remaining entities
print("\nCleaning up remaining entities...")
send_entity_erase(sock, addr, 1001)
send_entity_erase(sock, addr, 1003)
print("\nDone! All entities removed.")
sock.close()
if __name__ == '__main__':
main()