Compare commits
28 Commits
1a064440fb
...
c35349b891
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c35349b891 | ||
|
|
04e3257e12 | ||
|
|
30a20c9b30 | ||
|
|
6974ab8ee2 | ||
|
|
095140383a | ||
|
|
ad4dc7b0df | ||
|
|
1917dbb29c | ||
|
|
0398af67cf | ||
|
|
5c00af0c05 | ||
|
|
f5dee50cdb | ||
|
|
0325222354 | ||
|
|
28ec707d30 | ||
|
|
047edca7de | ||
|
|
f400c9272e | ||
|
|
e17b57b1e2 | ||
|
|
b091354f29 | ||
|
|
4c2026c3ba | ||
|
|
f4866dd8fe | ||
|
|
ba6ff968db | ||
|
|
9e955e7548 | ||
|
|
aa9fee158b | ||
|
|
502e055149 | ||
|
|
74265dd855 | ||
|
|
70f58a6d5e | ||
|
|
6ac8991bd3 | ||
|
|
62b1ba3db1 | ||
|
|
93e9ed3a12 | ||
|
|
a1d404c0a1 |
1
bridge/Cargo.lock
generated
1
bridge/Cargo.lock
generated
@@ -2513,6 +2513,7 @@ dependencies = [
|
||||
"serde",
|
||||
"stardust-xr-asteroids",
|
||||
"stardust-xr-fusion",
|
||||
"stardust-xr-molecules",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"zbus",
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
88
tools/inject_test_entities.py
Executable 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()
|
||||
Reference in New Issue
Block a user