1use serde::{Deserialize, Serialize};
7
8use crate::health::{HealthCheck, HealthCheckRequest};
9use crate::player::{PlayerId, PlayerState, DisconnectReason};
10use crate::server::{
11 ServerHeartbeat, ServerInfo, ServerRegistration, ServerId,
12 RegistrationResponse, SpawnServerRequest, SpawnServerResponse,
13};
14use crate::transfer::{TransferRequest, TransferToken};
15use crate::spatial::WorldCoordinate;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type", content = "payload")]
20pub enum HorizonMessage {
21 Register(ServerRegistration),
23
24 Heartbeat(ServerHeartbeat),
26
27 HealthResponse(HealthCheck),
29
30 PlayerConnected {
32 player_id: PlayerId,
33 position: WorldCoordinate,
34 },
35
36 PlayerDisconnected {
38 player_id: PlayerId,
39 reason: DisconnectReason,
40 },
41
42 PlayerPositionUpdate {
44 player_id: PlayerId,
45 position: WorldCoordinate,
46 velocity: WorldCoordinate,
47 },
48
49 TransferRequest(TransferRequest),
51
52 TransferComplete {
54 player_id: PlayerId,
55 success: bool,
56 error: Option<String>,
57 },
58
59 TransferAccepted {
61 player_id: PlayerId,
62 token_id: String,
63 },
64
65 Shutdown {
67 server_id: ServerId,
68 player_count: u32,
69 },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "type", content = "payload")]
75pub enum AtlasMessage {
76 RegistrationResponse(RegistrationResponse),
78
79 HealthCheckRequest(HealthCheckRequest),
81
82 InitiateTransfer {
84 player_id: PlayerId,
85 target_server: ServerInfo,
86 token: TransferToken,
87 },
88
89 AcceptTransfer {
91 token: TransferToken,
92 player_state: PlayerState,
93 },
94
95 CancelTransfer {
97 player_id: PlayerId,
98 reason: String,
99 },
100
101 PrepareShutdown {
103 deadline_secs: u32,
104 },
105
106 AdjacentServersUpdate {
108 servers: Vec<ServerInfo>,
109 },
110
111 ConfigUpdate {
113 config: serde_json::Value,
114 },
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", content = "payload")]
120pub enum AtlasToMaestroMessage {
121 SpawnServer(SpawnServerRequest),
123
124 StopServer {
126 instance_id: String,
127 graceful: bool,
128 },
129
130 GetServerStats {
132 instance_id: String,
133 },
134
135 ScaleCluster {
137 target_count: u32,
138 },
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(tag = "type", content = "payload")]
144pub enum MaestroMessage {
145 SpawnResponse(SpawnServerResponse),
147
148 ServerStopped {
150 instance_id: String,
151 exit_code: Option<i32>,
152 },
153
154 ServerStats {
156 instance_id: String,
157 cpu_percent: f32,
158 memory_mb: u32,
159 running: bool,
160 },
161
162 ClusterScaled {
164 current_count: u32,
165 target_count: u32,
166 },
167
168 Error {
170 operation: String,
171 message: String,
172 },
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct Envelope<T> {
178 pub id: String,
180 pub timestamp_ms: u64,
182 pub source: String,
184 pub destination: String,
186 pub message: T,
188}
189
190impl<T> Envelope<T> {
191 pub fn new(source: impl Into<String>, destination: impl Into<String>, message: T) -> Self {
193 use std::time::{SystemTime, UNIX_EPOCH};
194
195 Self {
196 id: uuid::Uuid::new_v4().to_string(),
197 timestamp_ms: SystemTime::now()
198 .duration_since(UNIX_EPOCH)
199 .unwrap()
200 .as_millis() as u64,
201 source: source.into(),
202 destination: destination.into(),
203 message,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Ack {
211 pub message_id: String,
213 pub success: bool,
215 #[serde(default)]
217 pub error: Option<String>,
218}
219
220impl Ack {
221 pub fn success(message_id: impl Into<String>) -> Self {
223 Self {
224 message_id: message_id.into(),
225 success: true,
226 error: None,
227 }
228 }
229
230 pub fn failure(message_id: impl Into<String>, error: impl Into<String>) -> Self {
232 Self {
233 message_id: message_id.into(),
234 success: false,
235 error: Some(error.into()),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_horizon_message_serialization() {
246 let msg = HorizonMessage::PlayerConnected {
247 player_id: PlayerId::new(),
248 position: WorldCoordinate::new(100.0, 50.0, -200.0),
249 };
250 let json = serde_json::to_string(&msg).unwrap();
251 let restored: HorizonMessage = serde_json::from_str(&json).unwrap();
252 match restored {
253 HorizonMessage::PlayerConnected { position, .. } => {
254 assert!((position.x - 100.0).abs() < 0.001);
255 }
256 _ => panic!("Wrong message type"),
257 }
258 }
259
260 #[test]
261 fn test_envelope_creation() {
262 let msg = Ack::success("test-123");
263 let envelope = Envelope::new("horizon-1", "atlas", msg);
264 assert_eq!(envelope.source, "horizon-1");
265 assert_eq!(envelope.destination, "atlas");
266 }
267}