1pub mod actions;
4use crate::client::llm_budget::call_llm_for_client;
5use crate::llm::actions::client_trait::{Client, ClientActionResult};
6use crate::logging::emit::Log;
7use crate::protocol::{ConnectContext, Event};
8use crate::server::minecraft::wire;
9use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
10use crate::state::{AccessLogOwner, ClientStatus};
11pub use actions::MinecraftClientProtocol;
12use anyhow::{Context, Result};
13use serde_json::{json, Value};
14use std::net::SocketAddr;
15use tokio::{
16 io::{AsyncReadExt, AsyncWriteExt},
17 net::TcpStream,
18 sync::mpsc,
19};
20
21pub const MAX_LOGIN_PACKETS: usize = 16;
23
24pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
25 let (host, port) = split_host_port(&ctx.remote_addr)?;
26 tokio::net::lookup_host((host.as_str(), port))
27 .await
28 .context("resolve the server address")?
29 .next()
30 .context("the server address resolved to nothing")?;
31 ctx.state
32 .update_client_status(ctx.client_id, ClientStatus::Connected)
33 .await;
34 let external =
35 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
36 let (internal_tx, internal_rx) = mpsc::channel::<Value>(16);
37 let (event_tx, mut event_rx) = mpsc::channel::<Event>(16);
38 event_tx.try_send(Event::new(
39 &actions::READY_EVENT,
40 json!({"remote_addr": ctx.remote_addr}),
41 ))?;
42 let events_ctx = ctx.clone();
43 let dispatcher = tokio::spawn(async move {
44 let protocol = MinecraftClientProtocol;
45 while let Some(event) = event_rx.recv().await {
46 let instruction = events_ctx
47 .state
48 .get_instruction_for_client(events_ctx.client_id)
49 .await
50 .unwrap_or_default();
51 let memory = events_ctx
52 .state
53 .get_memory_for_client(events_ctx.client_id)
54 .await
55 .unwrap_or_default();
56 match call_llm_for_client(
57 &events_ctx.llm_client,
58 &events_ctx.state,
59 events_ctx.client_id.to_string(),
60 &instruction,
61 &memory,
62 Some(&event),
63 &protocol,
64 &events_ctx.status_tx,
65 )
66 .await
67 {
68 Ok(result) => {
69 if let Some(memory) = result.memory_updates {
70 events_ctx
71 .state
72 .set_memory_for_client(events_ctx.client_id, memory)
73 .await;
74 }
75 for action in result.actions {
76 if internal_tx.send(action).await.is_err() {
77 return;
78 }
79 }
80 }
81 Err(e) => Log::new(Some(&events_ctx.status_tx))
82 .warn(format!("Minecraft client handler: {e}")),
83 }
84 }
85 });
86 let dispatcher_abort = dispatcher.abort_handle();
87 ctx.state
88 .register_client_task(ctx.client_id, dispatcher)
89 .await;
90 let session_ctx = ctx.clone();
91 let task = tokio::spawn(async move {
92 let result = session(&session_ctx, &host, port, external, internal_rx, event_tx).await;
93 if result.is_err() {
94 dispatcher_abort.abort();
95 }
96 let status = match result {
97 Ok(()) => ClientStatus::Disconnected,
98 Err(e) => {
99 Log::new(Some(&session_ctx.status_tx)).warn(format!("Minecraft client ended: {e}"));
100 ClientStatus::Error(e.to_string())
101 }
102 };
103 session_ctx
104 .state
105 .update_client_status(session_ctx.client_id, status)
106 .await;
107 session_ctx
108 .state
109 .remove_client_handle(session_ctx.client_id)
110 .await;
111 let _ = session_ctx.status_tx.send("__UPDATE_UI__".into());
112 });
113 ctx.state.register_client_task(ctx.client_id, task).await;
114 Ok("0.0.0.0:0".parse()?)
115}
116
117pub fn split_host_port(addr: &str) -> Result<(String, u16)> {
119 if let Some(rest) = addr.strip_prefix('[') {
120 let (host, tail) = rest.split_once(']').context("unclosed [ in address")?;
121 let port = match tail.strip_prefix(':') {
122 Some(p) => p.parse().context("port")?,
123 None => 25565,
124 };
125 return Ok((host.to_string(), port));
126 }
127 match addr.rsplit_once(':') {
128 Some((host, port)) if !host.contains(':') => {
129 Ok((host.to_string(), port.parse().context("port")?))
130 }
131 _ => Ok((addr.to_string(), 25565)),
132 }
133}
134
135struct Conn {
137 stream: TcpStream,
138 threshold: Option<i32>,
139}
140
141impl Conn {
142 async fn open(host: &str, port: u16) -> Result<Self> {
143 let stream = tokio::time::timeout(wire::IO_TIMEOUT, TcpStream::connect((host, port)))
144 .await
145 .context("Minecraft connect deadline")??;
146 Ok(Self {
147 stream,
148 threshold: None,
149 })
150 }
151
152 async fn write(&mut self, bytes: &[u8]) -> Result<()> {
153 tokio::time::timeout(wire::IO_TIMEOUT, self.stream.write_all(bytes))
154 .await
155 .context("Minecraft write deadline")??;
156 Ok(())
157 }
158
159 async fn send(&mut self, id: i32, body: &[u8]) -> Result<()> {
162 let bytes = if self.threshold.is_some() {
163 let mut inner = vec![0u8];
164 wire::put_varint(&mut inner, id);
165 inner.extend_from_slice(body);
166 let mut out = Vec::new();
167 wire::put_varint(&mut out, inner.len() as i32);
168 out.extend_from_slice(&inner);
169 out
170 } else {
171 wire::frame(id, body)
172 };
173 self.write(&bytes).await
174 }
175
176 async fn recv(&mut self) -> Result<(i32, Vec<u8>)> {
177 let raw = wire::read_raw(
178 &mut self.stream,
179 wire::MAX_CLIENTBOUND_PACKET,
180 wire::IO_TIMEOUT,
181 )
182 .await?
183 .context("server closed the connection")?;
184 if self.threshold.is_none() {
185 return wire::split_id(&raw);
186 }
187 let (data_len, n) = wire::get_varint(&raw)?;
188 if data_len == 0 {
189 return wire::split_id(&raw[n..]);
190 }
191 anyhow::ensure!(
192 data_len > 0 && data_len as usize <= wire::MAX_CLIENTBOUND_PACKET,
193 "compressed packet declares {data_len} bytes"
194 );
195 let mut inflated = Vec::new();
196 flate2::read::ZlibDecoder::new(&raw[n..])
197 .take(data_len as u64 + 1)
198 .read_to_end(&mut inflated)
199 .context("inflate a compressed packet")?;
200 anyhow::ensure!(
201 inflated.len() == data_len as usize,
202 "compressed packet inflated to {} bytes, not {data_len}",
203 inflated.len()
204 );
205 wire::split_id(&inflated)
206 }
207}
208
209use std::io::Read as _;
210
211async fn status(host: &str, port: u16, protocol: i32) -> Result<Value> {
212 let mut conn = Conn::open(host, port).await?;
213 let mut out = wire::encode_handshake(&wire::Handshake {
214 protocol_version: protocol,
215 server_address: host.to_string(),
216 server_port: port,
217 next_state: wire::STATE_STATUS,
218 });
219 out.extend_from_slice(&wire::frame(0x00, &[]));
220 conn.write(&out).await?;
221 let (id, body) = conn.recv().await?;
222 anyhow::ensure!(id == 0x00, "status answered with packet 0x{id:02x}");
223 let text = wire::Reader::new(&body).string(wire::MAX_JSON_CHARS)?;
224 let status: Value = serde_json::from_str(&text).context("status is not JSON")?;
225 let token = crate::utils::clock::SystemTime::now()
226 .duration_since(crate::utils::clock::UNIX_EPOCH)
227 .map(|d| d.as_millis() as i64)
228 .unwrap_or(1);
229 let started = crate::utils::clock::Instant::now();
230 conn.send(0x01, &token.to_be_bytes()).await?;
231 let latency = match conn.recv().await {
233 Ok((0x01, body)) if body == token.to_be_bytes() => {
234 Some(started.elapsed().as_secs_f64() * 1000.0)
235 }
236 _ => None,
237 };
238 let sample: Vec<Value> = status["players"]["sample"]
239 .as_array()
240 .map(|s| {
241 s.iter()
242 .take(wire::MAX_SAMPLE)
243 .map(|p| json!({"name": p["name"], "id": p["id"]}))
244 .collect()
245 })
246 .unwrap_or_default();
247 Ok(json!({
248 "legacy": false,
249 "version_name": status["version"]["name"],
250 "protocol": status["version"]["protocol"],
251 "online_players": status["players"]["online"],
252 "max_players": status["players"]["max"],
253 "motd": wire::plain_text(&status["description"]),
254 "sample": sample,
255 "has_favicon": status.get("favicon").is_some(),
256 "enforces_secure_chat": status.get("enforcesSecureChat"),
257 "latency_ms": latency,
258 }))
259}
260
261async fn legacy_status(host: &str, port: u16) -> Result<Value> {
262 let mut conn = Conn::open(host, port).await?;
263 conn.write(&wire::encode_legacy_ping(host, port)).await?;
264 let reply = tokio::time::timeout(wire::IO_TIMEOUT, async {
265 let kind = conn.stream.read_u8().await?;
266 anyhow::ensure!(kind == 0xFF, "legacy ping answered with 0x{kind:02x}");
267 let units = conn.stream.read_u16().await? as usize;
268 let mut text = vec![0u8; units * 2];
269 conn.stream.read_exact(&mut text).await?;
270 Ok(text)
271 })
272 .await
273 .context("legacy ping deadline")??;
274 let mut event = wire::decode_legacy_reply(&reply)?;
275 event["legacy"] = Value::Bool(true);
276 event["motd"] = json!(event["motd"].as_str().unwrap_or_default());
277 Ok(event)
278}
279
280async fn login(
281 host: &str,
282 port: u16,
283 protocol: i32,
284 username: &str,
285 uuid: [u8; 16],
286) -> Result<Value> {
287 let mut conn = Conn::open(host, port).await?;
288 let mut out = wire::encode_handshake(&wire::Handshake {
289 protocol_version: protocol,
290 server_address: host.to_string(),
291 server_port: port,
292 next_state: wire::STATE_LOGIN,
293 });
294 out.extend_from_slice(&wire::encode_login_start(protocol, username, &uuid));
295 conn.write(&out).await?;
296 let mut event = json!({});
297 for _ in 0..MAX_LOGIN_PACKETS {
298 let (id, body) = conn.recv().await?;
299 let mut r = wire::Reader::new(&body);
300 match id {
301 0x00 => {
302 let text = r.string(wire::MAX_JSON_CHARS)?;
303 let reason = serde_json::from_str::<Value>(&text)
304 .map(|v| wire::plain_text(&v))
305 .unwrap_or(text);
306 event["outcome"] = json!("disconnected");
307 event["reason"] = json!(reason);
308 return Ok(event);
309 }
310 0x01 => {
311 event["outcome"] = json!("encryption_required");
312 return Ok(event);
313 }
314 0x02 => {
315 let uuid = if protocol >= 735 {
316 wire::format_uuid(r.bytes(16)?)
317 } else {
318 r.string(36)?
319 };
320 event["outcome"] = json!("accepted");
321 event["uuid"] = json!(uuid);
322 event["username"] = json!(r.string(wire::MAX_USERNAME_CHARS)?);
323 return Ok(event);
324 }
325 0x03 => {
326 let threshold = r.varint()?;
327 event["compression_threshold"] = json!(threshold);
328 conn.threshold = Some(threshold);
329 }
330 0x04 => {
332 let message_id = r.varint()?;
333 let mut reply = Vec::new();
334 wire::put_varint(&mut reply, message_id);
335 reply.push(0);
336 conn.send(0x02, &reply).await?;
337 }
338 0x05 => {
340 let key = r.string(32767)?;
341 let mut reply = Vec::new();
342 wire::put_string(&mut reply, &key);
343 reply.push(0);
344 conn.send(0x04, &reply).await?;
345 }
346 other => anyhow::bail!("unexpected login packet 0x{other:02x}"),
347 }
348 }
349 anyhow::bail!("no login verdict within {MAX_LOGIN_PACKETS} packets")
350}
351
352async fn run(host: &str, port: u16, action: &Value) -> Result<(Event, Value)> {
353 match action["type"].as_str() {
354 Some("minecraft_status") => {
355 let data = status(host, port, actions::protocol_version(action)?).await?;
356 Ok((Event::new(&actions::STATUS_EVENT, data.clone()), data))
357 }
358 Some("minecraft_legacy_status") => {
359 let data = legacy_status(host, port).await?;
360 Ok((Event::new(&actions::STATUS_EVENT, data.clone()), data))
361 }
362 Some("minecraft_login") => {
363 let uuid = match action["uuid"].as_str() {
364 Some(u) => wire::parse_uuid(u)?,
365 None => [0u8; 16],
366 };
367 let data = login(
368 host,
369 port,
370 actions::protocol_version(action)?,
371 action["username"].as_str().unwrap_or_default(),
372 uuid,
373 )
374 .await?;
375 Ok((Event::new(&actions::LOGIN_EVENT, data.clone()), data))
376 }
377 _ => anyhow::bail!("Unknown Minecraft client action"),
378 }
379}
380
381async fn session(
382 ctx: &ConnectContext,
383 host: &str,
384 port: u16,
385 mut external: mpsc::Receiver<ClientCommand>,
386 mut internal: mpsc::Receiver<Value>,
387 events: mpsc::Sender<Event>,
388) -> Result<()> {
389 loop {
390 let (action, mut injected) = tokio::select! {
391 command = external.recv() => match command {
392 Some(c) => (c.action.clone(), Some(c)),
393 None => return Ok(()),
394 },
395 action = internal.recv() => match action {
396 Some(a) => (a, None),
397 None => return Ok(()),
398 },
399 };
400 match MinecraftClientProtocol.execute_action(action.clone()) {
401 Ok(ClientActionResult::Disconnect) => {
402 if let Some(command) = injected.take() {
403 crate::client::command_support::reply(
404 command,
405 Ok(ClientSendOutcome::Disconnected),
406 );
407 }
408 return Ok(());
409 }
410 Ok(_) => {}
411 Err(e) => {
412 if let Some(command) = injected.take() {
413 crate::client::command_support::reply(
414 command,
415 Ok(ClientSendOutcome::Rejected {
416 error: e.to_string(),
417 }),
418 );
419 }
420 continue;
421 }
422 }
423 let name = action["type"].as_str().unwrap_or_default().to_string();
424 if injected.is_some() {
425 ctx.state
426 .record_access_log(
427 AccessLogOwner::Client(ctx.client_id.as_u32()),
428 "Minecraft",
429 None,
430 "injected_action",
431 json!({"action": name}),
432 vec![],
433 )
434 .await;
435 }
436 match run(host, port, &action).await {
438 Ok((event, data)) => {
439 if let Some(command) = injected.take() {
440 crate::client::command_support::reply(
441 command,
442 Ok(ClientSendOutcome::Executed {
443 detail: data.to_string(),
444 }),
445 );
446 }
447 Log::new(Some(&ctx.status_tx)).info(format!("Minecraft {name}: {data}"));
448 events
449 .try_send(event)
450 .context("Minecraft event queue full; consumer stalled")?;
451 }
452 Err(e) => {
453 if let Some(command) = injected.take() {
454 crate::client::command_support::reply(
455 command,
456 Err(anyhow::anyhow!(e.to_string())),
457 );
458 }
459 Log::new(Some(&ctx.status_tx)).warn(format!("Minecraft {name} failed: {e:#}"));
460 }
461 }
462 }
463}