Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/arcane-infra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ arcane-wire = { path = "../arcane-wire" }
axum = { version = "0.7", optional = true }
futures-util = { version = "0.3", optional = true }
rayon = { version = "1", optional = true }
rapier3d = { version = "0.32", optional = true }
redis = "0.27"
reqwest = { version = "0.12", features = ["json", "blocking"], optional = true }
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -28,6 +29,7 @@ manager = ["dep:axum", "dep:tokio"]
cluster-ws = ["dep:tokio", "dep:tokio-tungstenite", "dep:futures-util", "dep:rayon"]
spacetimedb-persist = ["dep:reqwest"]
affinity-clustering = ["dep:arcane-affinity"]
rapier-cluster = ["cluster-ws", "dep:rapier3d"]

[[bin]]
name = "arcane-cluster"
Expand All @@ -39,5 +41,10 @@ name = "arcane-manager"
path = "src/bin/arcane_manager.rs"
required-features = ["manager"]

[[bin]]
name = "arcane-rapier-cluster"
path = "src/bin/arcane_rapier_cluster.rs"
required-features = ["rapier-cluster"]

[dev-dependencies]
redis = "0.27"
37 changes: 7 additions & 30 deletions crates/arcane-infra/src/bin/arcane_cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,29 @@
//! Example:
//! CLUSTER_ID=550e8400-e29b-41d4-a716-446655440000 cargo run -p arcane-infra --bin arcane-cluster --features cluster-ws

use std::env;
use std::sync::Arc;

use arcane_core::ClusterSimulation;
use arcane_infra::cluster_runner;
use uuid::Uuid;

fn parse_uuids(s: &str) -> Vec<Uuid> {
s.split(',')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.filter_map(|x| Uuid::parse_str(x).ok())
.collect()
}
#[cfg(feature = "cluster-ws")]
use arcane_infra::cluster_runner::{self, ClusterEnv};

fn main() -> Result<(), String> {
let cluster_id =
env::var("CLUSTER_ID").map_err(|_| "CLUSTER_ID env var required (UUID)".to_string())?;
let cluster_id =
Uuid::parse_str(&cluster_id).map_err(|e| format!("invalid CLUSTER_ID: {}", e))?;

let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let neighbor_ids = env::var("NEIGHBOR_IDS")
.map(|s| parse_uuids(&s))
.unwrap_or_default();

let ws_port: u16 = env::var("CLUSTER_WS_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080);

#[cfg(feature = "cluster-ws")]
{
let env = ClusterEnv::from_env()?;
cluster_runner::run_cluster_loop(
cluster_id,
redis_url,
neighbor_ids,
ws_port,
env.cluster_id,
env.redis_url,
env.neighbor_ids,
env.ws_port,
|_| vec![], // no demo entities; use arcane_cluster_demo from arcane-demo for that
Option::<Arc<dyn ClusterSimulation>>::None,
)
}

#[cfg(not(feature = "cluster-ws"))]
{
let _ = (cluster_id, redis_url, neighbor_ids, ws_port);
Err("cluster-ws feature required to run the cluster binary".to_string())
}
}
36 changes: 36 additions & 0 deletions crates/arcane-infra/src/bin/arcane_rapier_cluster.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Rapier-backed cluster server binary.
//!
//! Same env vars and command shape as `arcane_cluster.rs`. The only difference:
//! the user simulation is wrapped in [`arcane_infra::rapier_cluster::RapierClusterSim`],
//! so authoritative pose advancement happens through Rapier instead of the user's
//! `on_tick`. Networking, replication, neighbor merge, and persistence are
//! identical to the vanilla cluster.
//!
//! Env (same as arcane-cluster):
//! CLUSTER_ID β€” required; UUID of this cluster.
//! REDIS_URL β€” optional; default `redis://127.0.0.1:6379`.
//! NEIGHBOR_IDS β€” optional; comma-separated UUIDs of neighbor clusters.
//! CLUSTER_WS_PORT β€” optional; default 8080.

use std::sync::Arc;

use arcane_core::ClusterSimulation;
use arcane_infra::cluster_runner::{self, ClusterEnv};
use arcane_infra::{RapierClusterSim, RapierConfig};

fn main() -> Result<(), String> {
let env = ClusterEnv::from_env()?;

let user_sim: Option<Arc<dyn ClusterSimulation>> = None;
let rapier_sim: Arc<dyn ClusterSimulation> =
Arc::new(RapierClusterSim::new(user_sim, RapierConfig::default()));

cluster_runner::run_cluster_loop(
env.cluster_id,
env.redis_url,
env.neighbor_ids,
env.ws_port,
|_| vec![],
Some(rapier_sim),
)
}
43 changes: 43 additions & 0 deletions crates/arcane-infra/src/cluster_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,49 @@ const LOG_EVERY_TICKS: u64 = 100;
/// Log parseable server stats every N ticks (for benchmark: entities, clusters, tick_ms).
const LOG_STATS_EVERY_TICKS: u64 = 40;

/// Cluster-binary environment configuration (CLUSTER_ID, REDIS_URL,
/// NEIGHBOR_IDS, CLUSTER_WS_PORT). Shared by every cluster-binary entry point
/// so the env contract stays in one place.
#[derive(Clone, Debug)]
pub struct ClusterEnv {
pub cluster_id: Uuid,
pub redis_url: String,
pub neighbor_ids: Vec<Uuid>,
pub ws_port: u16,
}

impl ClusterEnv {
/// Read the standard cluster env vars. `CLUSTER_ID` is required; the rest
/// have defaults (Redis at `127.0.0.1:6379`, no neighbors, WS port `8080`).
pub fn from_env() -> Result<Self, String> {
let cluster_id = std::env::var("CLUSTER_ID")
.map_err(|_| "CLUSTER_ID env var required (UUID)".to_string())?;
let cluster_id =
Uuid::parse_str(&cluster_id).map_err(|e| format!("invalid CLUSTER_ID: {}", e))?;
let redis_url =
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let neighbor_ids = std::env::var("NEIGHBOR_IDS")
.map(|s| {
s.split(',')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.filter_map(|x| Uuid::parse_str(x).ok())
.collect()
})
.unwrap_or_default();
let ws_port: u16 = std::env::var("CLUSTER_WS_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080);
Ok(Self {
cluster_id,
redis_url,
neighbor_ids,
ws_port,
})
}
}

fn merge_with_neighbor_latest(
our_delta: EntityStateDelta,
neighbor_latest: &HashMap<Uuid, Vec<EntityStateEntry>>,
Expand Down
17 changes: 17 additions & 0 deletions crates/arcane-infra/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
//! - `ws_server`: client-facing WebSocket transport.
//! - `spacetimedb_persist`: throttled persistence adapter for state snapshots.
//! - `cluster_runner`: loop composition that wires server, replication, ws, and persistence.
//! - `rapier_cluster`: Rapier-backed authoritative physics wrapped as a `ClusterSimulation`
//! (feature `rapier-cluster`).

#[cfg(feature = "cluster-ws")]
pub mod broadcast_channel_cap;
Expand All @@ -31,6 +33,9 @@ pub mod cluster_stats;
#[cfg(feature = "cluster-ws")]
pub mod ws_server;

#[cfg(feature = "rapier-cluster")]
pub mod rapier_cluster;

#[cfg(feature = "cluster-ws")]
pub use arcane_core::cluster_simulation::{ClusterSimulation, ClusterTickContext, GameAction};

Expand All @@ -39,3 +44,15 @@ pub use cluster_server::ClusterServer;
pub use redis_channel::RedisReplicationChannel;
pub use replication_channel_manager::ReplicationChannelManager;
pub use rpc_handler::RpcHandler;

#[cfg(feature = "rapier-cluster")]
pub use rapier_cluster::{
ContactEvent, JointId, JointSpec, PhysicsHandle, RapierBodyKind, RapierClusterSim,
RapierClusterSimulation, RapierClusterTickContext, RapierColliderShape, RapierCollisionGroups,
RapierConfig, RapierMaterial, RaycastHit,
};

// Re-export Rapier's `Group` so users of `RapierCollisionGroups` can construct
// memberships/filter values without depending on rapier3d directly.
#[cfg(feature = "rapier-cluster")]
pub use rapier3d::geometry::Group;
Loading
Loading