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
Empty file added .epic-init
Empty file.
5 changes: 5 additions & 0 deletions crates/arcane-core/src/cluster_simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ pub struct GameAction {
/// remove it and include the id in the next delta's `removed` list. Deleting from `entities` alone
/// would omit the entity from `updated` without a removal record.
pub struct ClusterTickContext<'a> {
/// Unique identifier for this cluster.
pub cluster_id: Uuid,
/// Monotonic tick index that will be assigned to the upcoming replication delta's `tick` field.
pub tick: u64,
/// Simulation time step (seconds) since the last tick.
pub dt_seconds: f64,
/// Mutable reference to the cluster's entity storage.
pub entities: &'a mut HashMap<Uuid, EntityStateEntry>,
/// Processed after `on_tick` returns so the next delta lists these ids under `removed`.
pub pending_removals: &'a mut Vec<Uuid>,
Expand All @@ -59,5 +62,7 @@ pub struct ClusterTickContext<'a> {

/// Custom simulation step for entities owned by this cluster.
pub trait ClusterSimulation: Send + Sync {
/// Advance simulation state by one tick. Called once per tick after client updates and injected
/// entities are applied, and before the replication delta is built.
fn on_tick(&self, ctx: &mut ClusterTickContext<'_>);
}
42 changes: 42 additions & 0 deletions crates/arcane-core/src/clustering_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,92 @@ use uuid::Uuid;
/// View of world state passed to the clustering model. ClusterManager maintains this from SpacetimeDB subscriptions.
#[derive(Clone, Debug)]
pub struct WorldStateView {
/// Monotonic timestamp (seconds since epoch) of the snapshot.
pub timestamp: f64,
/// Maximum wall-clock ms the model may spend on this evaluation cycle.
pub evaluation_budget_ms: u32,
/// All clusters visible in the live view at this instant.
pub clusters: Vec<ClusterInfo>,
/// All players visible in the live view at this instant.
pub players: Vec<PlayerInfo>,
}

/// Per-cluster info in the live view.
#[derive(Clone, Debug)]
pub struct ClusterInfo {
/// Unique identifier for this cluster.
pub cluster_id: Uuid,
/// Hostname of the server serving this cluster.
pub server_host: String,
/// Player UUIDs assigned to this cluster.
pub player_ids: Vec<Uuid>,
/// Cached player count (derived from `player_ids.len()`).
pub player_count: u32,
/// CPU utilisation as a percentage (0.0–100.0).
pub cpu_pct: f32,
/// Spatial centroid of the cluster's players in world coordinates.
pub centroid: Vec2,
/// Maximum distance of any player from the centroid.
pub spread_radius: f32,
/// Outbound cross-cluster RPCs per second from this cluster.
pub rpc_rate_out: f32,
}

/// Per-player info in the live view.
#[derive(Clone, Debug)]
pub struct PlayerInfo {
/// Unique identifier for this player.
pub player_id: Uuid,
/// Cluster the player is currently assigned to.
pub cluster_id: Uuid,
/// Current world-space position.
pub position: Vec2,
/// Current velocity vector (units/second).
pub velocity: Vec2,
/// Guild this player belongs to, if any.
pub guild_id: Option<Uuid>,
/// Party this player belongs to, if any.
pub party_id: Option<Uuid>,
}

/// A single merge or split decision from the model.
#[derive(Clone, Debug)]
pub struct ClusterDecision {
/// Whether this is a merge or split decision.
pub decision_type: DecisionType,
/// Execution priority (1 = highest, 10 = lowest).
pub priority: u8,
/// Machine- and human-readable reason for the decision.
pub reason: DecisionReason,
/// Model confidence (0.0–1.0). Static rules always return 1.0.
pub confidence: f32,
/// For merge decisions: the source cluster whose players move to `target_cluster_id`.
pub source_cluster_id: Option<Uuid>,
/// For merge decisions: the target cluster receiving the source cluster's players.
pub target_cluster_id: Option<Uuid>,
/// For split decisions: the cluster being split into two groups.
pub cluster_id: Option<Uuid>,
/// For split decisions: first subgroup of player UUIDs.
pub split_group_a: Option<Vec<Uuid>>,
/// For split decisions: second subgroup of player UUIDs.
pub split_group_b: Option<Vec<Uuid>>,
}

/// Whether a decision proposes merging two clusters or splitting one cluster into two.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DecisionType {
/// Combine `source_cluster_id` into `target_cluster_id`.
Merge,
/// Divide `cluster_id` into two groups.
Split,
}

/// Machine- and human-readable reason code for a clustering decision.
#[derive(Clone, Debug)]
pub struct DecisionReason {
/// Machine-readable code (e.g. `"PARTY_SEPARATED"`, `"SPATIAL_PROXIMITY"`).
pub code: String,
/// Human-readable explanation for logging and debugging.
pub detail: String,
}

Expand All @@ -77,17 +110,26 @@ pub trait IClusteringModel: Send + Sync {
fn validate_view(&self, view: &WorldStateView) -> ValidationResult;
}

/// Metadata describing a clustering model implementation.
#[derive(Clone, Debug)]
pub struct ModelInfo {
/// Human-readable model type (e.g. `"static_rules"`, `"ml_model"`).
pub model_type: String,
/// Semantic version or build identifier for the model.
pub version: String,
/// Unix timestamp of the model's training, if applicable.
pub trained_at: Option<f64>,
/// Number of features the ML model was trained on, if applicable.
pub feature_count: Option<u32>,
}

/// Outcome of a `validate_view` call: whether the view is structurally valid and any diagnostics.
#[derive(Clone, Debug)]
pub struct ValidationResult {
/// Whether the view passed all validation checks.
pub valid: bool,
/// Non-fatal diagnostics that may be of interest to operators.
pub warnings: Vec<String>,
/// Fatal validation failures that make the view unsuitable for evaluation.
pub errors: Vec<String>,
}
29 changes: 24 additions & 5 deletions crates/arcane-core/src/replication_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,33 @@
use crate::types::Vec3;
use uuid::Uuid;

/// Configuration for a replication channel (one neighbor).
/// Configuration for a replication channel to one neighbor.
#[derive(Clone, Debug)]
pub struct ChannelConfig {
/// Spatial radius (world units) within which entities are replicated to this neighbor.
pub observation_radius: f64,
/// Maximum number of pending deltas before the channel starts dropping.
pub max_queue_depth: usize,
/// Minimum interval (ms) between consecutive sends to this neighbor.
pub send_interval_ms: u32,
/// Whether to compress delta payloads before transmission.
pub compression_enabled: bool,
}

/// Entity state delta sent to a neighbor. Fire-and-forget; no ack.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntityStateDelta {
/// Cluster that produced and sent this delta.
pub source_cluster_id: Uuid,
/// Monotonic sequence number for ordering deltas from this source.
pub seq: i64,
/// Simulation tick at which this delta was produced.
pub tick: u64,
/// Monotonic timestamp (seconds since epoch) of the snapshot.
pub timestamp: f64,
/// Entities that were created or updated since the last delta.
pub updated: Vec<EntityStateEntry>,
/// Entity UUIDs that were removed since the last delta.
pub removed: Vec<Uuid>,
}

Expand All @@ -47,10 +57,13 @@ pub struct EntityStateDelta {
/// `local_data` uses [`serde::Serialize`] with skip — it does not cross the replication mesh.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntityStateEntry {
/// Unique identifier for this entity (bucket 1 — spine, routing).
pub entity_id: Uuid,
/// Cluster that owns this entity (for client colorization and ownership display).
pub cluster_id: Uuid,
/// World-space position of the entity (bucket 1 — spine, pose).
pub position: Vec3,
/// Velocity vector in world units per second (bucket 1 — spine, pose).
pub velocity: Vec3,
/// **Bucket 2** — replicated game JSON (neighbors, clients). Default `null`; omitted when null.
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
Expand All @@ -64,6 +77,8 @@ pub struct EntityStateEntry {
}

impl EntityStateEntry {
/// Create a new entry with only bucket-1 spine fields. `user_data` and `local_data` start as
/// [`serde_json::Value::Null`]; callers set them after construction when needed.
pub fn new(entity_id: Uuid, cluster_id: Uuid, position: Vec3, velocity: Vec3) -> Self {
Self {
entity_id,
Expand All @@ -76,20 +91,24 @@ impl EntityStateEntry {
}
}

/// Reason for closing a channel.
/// Reason a replication channel was closed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CloseReason {
/// The neighbor cluster left the region or disconnected.
NeighborDepartured,
/// The two clusters merged into one; the channel between them is no longer needed.
ClustersMerged,
/// The local cluster or whole simulation is shutting down.
Shutdown,
}

/// Contract for publishing/subscribing to a neighbor's state. One instance per neighbor.
/// Contract for publishing/subscribing to a neighbor cluster's entity state. One instance per neighbor.
pub trait IReplicationChannel: Send + Sync {
/// Enqueue a delta for transmission. Non-blocking; may drop if queue full.
/// Enqueue a delta for transmission. Non-blocking; may return a congestion signal or silently
/// drop when the queue is full.
fn send(&self, delta: EntityStateDelta);

/// Close the channel and flush pending sends.
/// Close the channel, flush pending sends, and release transport resources.
fn close(&self, reason: CloseReason);
}

Expand Down
25 changes: 25 additions & 0 deletions crates/arcane-core/src/server_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,54 @@ use uuid::Uuid;
/// Handle to an allocated cluster server.
#[derive(Clone, Debug)]
pub struct ServerHandle {
/// Unique identifier for this server instance.
pub server_id: Uuid,
/// Hostname or IP address of the server.
pub host: String,
/// WebSocket port for client connections.
pub ws_port: u16,
/// RPC port for inter-cluster communication.
pub rpc_port: u16,
/// Metrics / observability port.
pub metrics_port: u16,
/// Monotonic timestamp (seconds since epoch) when the server was allocated.
pub allocated_at: f64,
}

/// Error from pool operations.
#[derive(Clone, Debug)]
pub struct PoolError {
/// Machine-readable error code.
pub code: PoolErrorCode,
/// Human-readable error detail for logging and debugging.
pub detail: String,
}

/// Reasons a pool operation can fail.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PoolErrorCode {
/// All servers are allocated; no capacity remaining.
PoolExhausted,
/// An allocation request exceeded the timeout.
AllocationTimeout,
/// The target server is in an unhealthy state.
ServerUnhealthy,
}

/// Current pool capacity and health.
#[derive(Clone, Debug)]
pub struct PoolStatus {
/// Total number of server slots in the pool.
pub total_capacity: u32,
/// Number of servers currently free for allocation.
pub available: u32,
/// Number of servers currently allocated to clusters.
pub allocated: u32,
/// Number of servers in a failed state.
pub failed: u32,
/// Minimum desired free capacity; below this triggers scaling.
pub min_available: u32,
/// P99 allocation latency in milliseconds.
pub allocation_p99_ms: f32,
}

Expand All @@ -56,15 +74,22 @@ pub trait IServerPool: Send + Sync {
fn get_status(&self) -> PoolStatus;
}

/// Category of server failure reported to the pool.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FailureType {
/// Server is not reachable over the network.
Unreachable,
/// Server process crashed (e.g. segfault, OOM).
SimulationCrashed,
/// Server is reachable but operating outside acceptable performance bounds.
PerformanceDegraded,
}

/// Result of a failure report: an optional replacement server and expected wait time.
#[derive(Clone, Debug)]
pub struct ReplacementHandle {
/// Replacement server handle, if one was immediately available.
pub handle: Option<ServerHandle>,
/// Estimated time (ms) until a replacement is ready, if not immediate.
pub eta_ms: Option<u32>,
}
11 changes: 11 additions & 0 deletions crates/arcane-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ pub use uuid::Uuid as EntityId;
/// 2D vector (e.g. centroid in 2D plane, or x/z).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vec2 {
/// X-axis component.
pub x: f64,
/// Y-axis component.
pub y: f64,
}

impl Vec2 {
/// Create a new 2D vector from its x and y components.
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
Expand All @@ -24,12 +27,16 @@ impl Vec2 {
/// 3D position (world position, centroid with height).
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Vec3 {
/// X-axis coordinate.
pub x: f64,
/// Y-axis coordinate.
pub y: f64,
/// Z-axis coordinate.
pub z: f64,
}

impl Vec3 {
/// Create a new 3D vector from its x, y, and z components.
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
Expand All @@ -46,9 +53,13 @@ impl Vec3 {
/// Per-cluster geometry from SpatialIndex (IN-03). Used for neighbor lists and WorldStateView.
#[derive(Clone, Debug, PartialEq)]
pub struct ClusterGeometry {
/// Unique identifier for this cluster.
pub cluster_id: Uuid,
/// Spatial centroid of the cluster.
pub centroid: Vec3,
/// Maximum distance of any entity from the centroid.
pub spread_radius: f64,
/// Number of entities assigned to this cluster.
pub entity_count: u32,
}

Expand Down
14 changes: 14 additions & 0 deletions crates/arcane-core/src/world_simulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,43 @@ use uuid::Uuid;
/// Last known state of an entity when it was last observed.
#[derive(Clone, Debug)]
pub struct LastKnownState {
/// Unique identifier for this entity.
pub entity_id: Uuid,
/// Timestamp (seconds since epoch) when the entity was last observed.
pub last_observed: f64,
/// Last observed world-space position.
pub position: Vec3,
/// Last observed velocity vector (units per second).
pub velocity: Vec3,
/// Current health value.
pub health: i32,
/// Maximum health value.
pub health_max: i32,
/// Game-defined behavior state (e.g. `"idle"`, `"patrolling"`).
pub behavior_state: String,
/// Spawn or anchor position the entity returns to.
pub home_position: Vec3,
/// Distance from home_position the entity may wander before returning.
pub territory_radius: f64,
}

/// Context at simulation time (optional, for FastForward/ML).
#[derive(Clone, Debug)]
pub struct WorldContext {
/// Current simulation timestamp (seconds since epoch).
pub current_time: f64,
}

/// Plausible state after simulating forward from last_known.
#[derive(Clone, Debug)]
pub struct SimulatedState {
/// Unique identifier for this entity.
pub entity_id: Uuid,
/// Simulated world-space position.
pub position: Vec3,
/// Simulated velocity vector (units per second).
pub velocity: Vec3,
/// Simulated health value.
pub health: i32,
}

Expand Down
Loading