1pub mod actions;
5pub mod wire;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::connection::ConnectionId;
12use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
13use anyhow::{Context, Result};
14use serde_json::{json, Value};
15use std::{net::SocketAddr, time::Duration};
16use tokio::{
17 io::{AsyncReadExt, AsyncWriteExt},
18 net::{TcpListener, TcpStream},
19};
20
21pub const MAX_IDLE_TIMEOUT_SECS: u64 = 300;
23
24fn idle(ctx: &SpawnContext) -> Result<Duration> {
25 let secs = ctx
26 .startup_params
27 .as_ref()
28 .map(|p| p.get_optional_u64("idle_timeout_secs"))
29 .transpose()?
30 .flatten()
31 .unwrap_or(wire::IO_TIMEOUT.as_secs());
32 anyhow::ensure!(
33 (1..=MAX_IDLE_TIMEOUT_SECS).contains(&secs),
34 "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
35 );
36 Ok(Duration::from_secs(secs))
37}
38
39pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
40 let idle = idle(&ctx)?;
41 let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
42 let addr = listener.local_addr()?;
43 Log::new(Some(&ctx.status_tx)).info(format!("Minecraft listening on {addr}"));
44 let state = ctx.state.clone();
45 let server_id = ctx.server_id;
46 let accept = tokio::spawn(async move {
47 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
48 crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
49 );
50 loop {
51 let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
52 &listener,
53 &limiter,
54 b"",
55 "Minecraft",
56 Some(&ctx.status_tx),
57 )
58 .await
59 {
60 Ok(v) => v,
61 Err(_) => break,
62 };
63 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
64 let now = crate::utils::clock::Instant::now();
65 ctx.state
66 .add_connection_to_server(
67 server_id,
68 ConnectionState {
69 id,
70 remote_addr: peer,
71 local_addr: addr,
72 bytes_sent: 0,
73 bytes_received: 0,
74 packets_sent: 0,
75 packets_received: 0,
76 last_activity: now,
77 status: ConnectionStatus::Active,
78 status_changed_at: now,
79 protocol_info: ProtocolConnectionInfo::empty(),
80 },
81 )
82 .await;
83 let child = ctx.clone();
84 ctx.state
85 .spawn_server_task(server_id, async move {
86 let _permit = permit;
87 if let Err(e) = session(&child, id, socket, peer, idle).await {
88 Log::new(Some(&child.status_tx))
89 .warn(format!("Minecraft connection {id} ended: {e}"));
90 }
91 child
92 .state
93 .update_connection_status(server_id, id, ConnectionStatus::Closed)
94 .await;
95 let _ = child.status_tx.send("__UPDATE_UI__".into());
96 })
97 .await;
98 }
99 });
100 state.register_server_task(server_id, accept).await;
101 Ok(addr)
102}
103
104async fn send<W: tokio::io::AsyncWrite + Unpin>(
105 ctx: &SpawnContext,
106 id: ConnectionId,
107 w: &mut W,
108 bytes: &[u8],
109) -> Result<()> {
110 tokio::time::timeout(wire::IO_TIMEOUT, async {
111 w.write_all(bytes).await?;
112 w.flush().await
113 })
114 .await
115 .context("Minecraft write deadline")??;
116 ctx.state
117 .update_connection_stats(
118 ctx.server_id,
119 id,
120 None,
121 Some(bytes.len() as u64),
122 None,
123 Some(1),
124 )
125 .await;
126 Ok(())
127}
128
129async fn received(ctx: &SpawnContext, id: ConnectionId, len: usize) {
130 ctx.state
131 .update_connection_stats(ctx.server_id, id, Some(len as u64), None, Some(1), None)
132 .await;
133}
134
135fn outcome(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
136 let summary = format!("Minecraft connection {id} operation={operation} decision={decision}");
137 let log = Log::new(Some(&ctx.status_tx));
138 if decision.starts_with("fail_closed_") || decision == "model_silent" {
139 log.error(summary);
140 } else {
141 log.info(summary);
142 }
143}
144
145async fn decide(
148 ctx: &SpawnContext,
149 id: ConnectionId,
150 event: Event,
151) -> Result<Option<(String, Value)>> {
152 let result = match call_llm(
153 &ctx.llm_client,
154 &ctx.state,
155 ctx.server_id,
156 Some(id),
157 &event,
158 &actions::MinecraftProtocol,
159 )
160 .await
161 {
162 Ok(result) => result,
163 Err(error) => {
164 outcome(ctx, id, event.id(), "fail_closed_llm_error");
165 return Err(error);
166 }
167 };
168 if !result.failures.is_empty() {
169 outcome(ctx, id, event.id(), "fail_closed_invalid_reply");
170 anyhow::bail!("Minecraft handler supplied an invalid action");
171 }
172 let mut found = None;
173 let mut pending = result.protocol_results;
174 while let Some(result) = pending.pop() {
175 match result {
176 ActionResult::Custom { name, data } if name.starts_with("minecraft_") => {
177 if found.is_some() {
178 outcome(ctx, id, event.id(), "fail_closed_invalid_reply");
179 anyhow::bail!("Multiple Minecraft answers to one request");
180 }
181 found = Some((name, data));
182 }
183 ActionResult::Multiple(items) => pending.extend(items),
184 _ => {}
185 }
186 }
187 if found.is_none() {
188 outcome(ctx, id, event.id(), "model_silent");
189 }
190 Ok(found)
191}
192
193async fn status_answer(ctx: &SpawnContext, id: ConnectionId, event: Event) -> Option<Value> {
195 match decide(ctx, id, event).await {
196 Ok(Some((name, data))) if name == "minecraft_status" => {
197 outcome(ctx, id, "minecraft_status_request", "model_answer");
198 Some(data)
199 }
200 Ok(Some((name, _))) if name == "minecraft_refuse" => {
201 outcome(ctx, id, "minecraft_status_request", "model_reject");
202 None
203 }
204 Ok(Some(_)) => {
205 outcome(
206 ctx,
207 id,
208 "minecraft_status_request",
209 "fail_closed_invalid_reply",
210 );
211 None
212 }
213 Ok(None) | Err(_) => None,
216 }
217}
218
219async fn session(
220 ctx: &SpawnContext,
221 id: ConnectionId,
222 socket: TcpStream,
223 peer: SocketAddr,
224 idle: Duration,
225) -> Result<()> {
226 let (mut reader, mut writer) = tokio::io::split(socket);
227 let first = match tokio::time::timeout(idle, reader.read_u8()).await {
228 Ok(Ok(b)) => b,
229 Ok(Err(_)) => return Ok(()),
230 Err(_) => anyhow::bail!("no handshake within {}s", idle.as_secs()),
231 };
232 if first == 0xFE {
233 let ping = wire::read_legacy_ping(&mut reader).await?;
234 received(ctx, id, 1).await;
235 let event = Event::new(
236 &actions::STATUS_EVENT,
237 json!({
238 "protocol_version": ping.protocol_version,
239 "server_address": ping.server_address,
240 "server_port": ping.server_port,
241 "legacy": true,
242 "remote_addr": peer.to_string(),
243 }),
244 );
245 if let Some(answer) = status_answer(ctx, id, event).await {
246 let reply = wire::encode_legacy_reply(&ping, &answer)?;
247 send(ctx, id, &mut writer, &reply).await?;
248 }
249 return Ok(());
250 }
251 let first = [first];
252 let mut chained = (&first[..]).chain(&mut reader);
253 let Some((packet_id, body)) =
254 wire::read_packet(&mut chained, wire::MAX_SERVERBOUND_PACKET, idle).await?
255 else {
256 return Ok(());
257 };
258 received(ctx, id, body.len() + 2).await;
259 anyhow::ensure!(
260 packet_id == 0x00,
261 "first packet is 0x{packet_id:02x}, not a handshake"
262 );
263 let handshake = wire::parse_handshake(&body)?;
264 if handshake.next_state == wire::STATE_STATUS {
265 status_state(ctx, id, &mut reader, &mut writer, peer, idle, &handshake).await
266 } else {
267 login_state(ctx, id, &mut reader, &mut writer, peer, idle, &handshake).await
268 }
269}
270
271async fn status_state<R, W>(
272 ctx: &SpawnContext,
273 id: ConnectionId,
274 reader: &mut R,
275 writer: &mut W,
276 peer: SocketAddr,
277 idle: Duration,
278 handshake: &wire::Handshake,
279) -> Result<()>
280where
281 R: tokio::io::AsyncRead + Unpin,
282 W: tokio::io::AsyncWrite + Unpin,
283{
284 let mut answered = false;
285 loop {
286 let Some((packet_id, body)) =
287 wire::read_packet(reader, wire::MAX_SERVERBOUND_PACKET, idle).await?
288 else {
289 return Ok(());
290 };
291 received(ctx, id, body.len() + 2).await;
292 match packet_id {
293 0x00 if !answered && body.is_empty() => {
295 answered = true;
296 let event = Event::new(
297 &actions::STATUS_EVENT,
298 json!({
299 "protocol_version": handshake.protocol_version,
300 "server_address": handshake.server_address,
301 "server_port": handshake.server_port,
302 "legacy": false,
303 "remote_addr": peer.to_string(),
304 }),
305 );
306 let Some(answer) = status_answer(ctx, id, event).await else {
307 return Ok(());
308 };
309 let json = wire::status_json(&answer, handshake.protocol_version)?;
310 let mut body = Vec::new();
311 wire::put_string(&mut body, &json);
312 send(ctx, id, writer, &wire::frame(0x00, &body)).await?;
313 }
314 0x01 if body.len() == 8 => {
316 send(ctx, id, writer, &wire::frame(0x01, &body)).await?;
317 return Ok(());
318 }
319 other => anyhow::bail!(
320 "unexpected status packet 0x{other:02x} ({} bytes)",
321 body.len()
322 ),
323 }
324 }
325}
326
327async fn login_state<R, W>(
328 ctx: &SpawnContext,
329 id: ConnectionId,
330 reader: &mut R,
331 writer: &mut W,
332 peer: SocketAddr,
333 idle: Duration,
334 handshake: &wire::Handshake,
335) -> Result<()>
336where
337 R: tokio::io::AsyncRead + Unpin,
338 W: tokio::io::AsyncWrite + Unpin,
339{
340 let Some((packet_id, body)) =
341 wire::read_packet(reader, wire::MAX_SERVERBOUND_PACKET, idle).await?
342 else {
343 return Ok(());
344 };
345 received(ctx, id, body.len() + 2).await;
346 anyhow::ensure!(
347 packet_id == 0x00,
348 "login packet 0x{packet_id:02x} is not Login Start"
349 );
350 let (username, uuid) = wire::parse_login_start(&body)?;
351 let event = Event::new(
352 &actions::LOGIN_EVENT,
353 json!({
354 "username": username,
355 "uuid": uuid,
356 "protocol_version": handshake.protocol_version,
357 "server_address": handshake.server_address,
358 "server_port": handshake.server_port,
359 "transfer": handshake.next_state == wire::STATE_TRANSFER,
360 "remote_addr": peer.to_string(),
361 }),
362 );
363 let reason = match decide(ctx, id, event).await {
364 Ok(Some((name, data))) if name == "minecraft_disconnect" => {
365 outcome(ctx, id, "minecraft_login", "model_answer");
366 data["reason"].as_str().unwrap_or_default().to_string()
367 }
368 Ok(Some((name, _))) if name == "minecraft_refuse" => {
369 outcome(ctx, id, "minecraft_login", "model_reject");
370 return Ok(());
371 }
372 Ok(Some(_)) => {
373 outcome(ctx, id, "minecraft_login", "fail_closed_invalid_reply");
374 crate::utils::WireFailure::Unavailable
375 .prefixed_text()
376 .to_string()
377 }
378 Ok(None) => crate::utils::WireFailure::Unavailable
380 .prefixed_text()
381 .to_string(),
382 Err(e) => crate::utils::wire_failure::prefixed_wire_failure_text(&e).to_string(),
383 };
384 let mut body = Vec::new();
385 wire::put_string(&mut body, &wire::disconnect_json(&reason)?);
386 send(ctx, id, writer, &wire::frame(0x00, &body)).await
387}