1use crate::{
20 command::{summon_named_entity_command, AddTagOutput, SummonNamedEntityOutput},
21 io::{create_dir_all, io_error, remove_dir, remove_dir_all, write, IoErrorAtPath},
22 log::LogEvent,
23 on_drop::OnDrop,
24 read_incremented_id, Command, ExecuteCommandsError, ExecuteCommandsErrorInner,
25 MinecraftConnection,
26};
27use indexmap::IndexSet;
28use log::error;
29use serde::{Deserialize, Serialize};
30use std::{
31 fmt::Display,
32 fs::{File, OpenOptions},
33 io::{BufReader, BufWriter, Seek, SeekFrom},
34 path::Path,
35 sync::atomic::{AtomicBool, Ordering},
36};
37use tokio_stream::StreamExt;
38use walkdir::WalkDir;
39
40#[derive(Debug)]
42pub struct ConnectError {
43 inner: ConnectErrorInner,
44}
45#[derive(Debug)]
46enum ConnectErrorInner {
47 Io(IoErrorAtPath),
48 Cancelled,
49}
50impl ConnectError {
51 fn new(inner: ConnectErrorInner) -> ConnectError {
52 ConnectError { inner }
53 }
54
55 pub fn is_cancelled(&self) -> bool {
58 matches!(self.inner, ConnectErrorInner::Cancelled)
59 }
60}
61impl From<IoErrorAtPath> for ConnectError {
62 fn from(value: IoErrorAtPath) -> ConnectError {
63 ConnectError::new(ConnectErrorInner::Io(value))
64 }
65}
66impl From<ExecuteCommandsError> for ConnectError {
67 fn from(value: ExecuteCommandsError) -> ConnectError {
68 match value.inner {
69 ExecuteCommandsErrorInner::Io(error) => error.into(),
70 }
71 }
72}
73impl Display for ConnectError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match &self.inner {
76 ConnectErrorInner::Io(error) => error.fmt(f),
77 ConnectErrorInner::Cancelled => write!(f, "Cancelled"),
78 }
79 }
80}
81impl std::error::Error for ConnectError {}
82impl From<ConnectError> for std::io::Error {
83 fn from(value: ConnectError) -> std::io::Error {
84 match value.inner {
85 ConnectErrorInner::Io(error) => std::io::Error::from(error),
86 ConnectErrorInner::Cancelled => {
87 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, value)
88 }
89 }
90 }
91}
92
93pub(crate) async fn connect(connection: &mut MinecraftConnection) -> Result<(), ConnectError> {
94 connection.create_datapack()?;
95
96 let success = AtomicBool::new(false);
97 let identifier = connection.identifier.clone();
98 let datapack_dir = connection.datapack_dir.clone();
99 let on_drop = OnDrop::new(|| {
101 remove_connector(&identifier, &datapack_dir);
103 if !success.load(Ordering::Relaxed) {
104 remove_disconnector(&identifier, &datapack_dir);
105 }
106 remove_empty_dirs(&datapack_dir);
107 });
108
109 let structure_id = {
110 let path = connection.structures_dir.join("id.txt");
111 match File::open(&path) {
112 Ok(mut file) => read_incremented_id(&mut file, &path)?,
113 Err(_) => 0,
114 }
115 };
116
117 create_connector(&identifier, structure_id, &datapack_dir)?;
118 create_disconnector(&identifier, &datapack_dir)?;
119 wait_for_connection(connection).await?;
120 success.store(true, Ordering::Relaxed);
121 drop(on_drop);
122 connection.execute_commands([Command::new(format!(
123 "function minect_internal:connection/{}/connect/reload",
124 identifier
125 ))])?;
126
127 Ok(())
128}
129
130fn create_connector(
131 identifier: &str,
132 structure_id: u64,
133 datapack_dir: impl AsRef<Path>,
134) -> Result<(), IoErrorAtPath> {
135 let expand_template = |template: &str| {
136 expand_template(template, identifier).replace("-structure_id-", &structure_id.to_string())
137 };
138 let datapack_dir = datapack_dir.as_ref();
139
140 macro_rules! add_to_function_tag {
141 ($relative_path:expr) => {{
142 let path = datapack_dir.join($relative_path);
143 let template = expand_template(include_datapack_template!($relative_path));
144 add_to_function_tag(path, &template)
145 }};
146 }
147 add_to_function_tag!("data/minect_internal/tags/functions/connect/choose_chunk.json")?;
148 add_to_function_tag!("data/minect_internal/tags/functions/connect/prompt.json")?;
149
150 macro_rules! expand {
151 ($relative_path:expr) => {{
152 let path = datapack_dir.join(expand_template($relative_path));
153 let contents = expand_template(include_datapack_template!($relative_path));
154 write(path, &contents)
155 }};
156 }
157 expand!("data/minect_internal/functions/connection/-connection_id-/connect/cancel_cleanup.mcfunction")?;
158 expand!("data/minect_internal/functions/connection/-connection_id-/connect/cancel.mcfunction")?;
159 expand!(
160 "data/minect_internal/functions/connection/-connection_id-/connect/choose_chunk_unchecked.mcfunction"
161 )?;
162 expand!(
163 "data/minect_internal/functions/connection/-connection_id-/connect/choose_chunk.mcfunction"
164 )?;
165 expand!("data/minect_internal/functions/connection/-connection_id-/connect/confirm_chunk.mcfunction")?;
166 expand!("data/minect_internal/functions/connection/-connection_id-/connect/prompt_unchecked.mcfunction")?;
167 expand!("data/minect_internal/functions/connection/-connection_id-/connect/prompt.mcfunction")?;
168 expand!("data/minect_internal/functions/connection/-connection_id-/connect/reload.mcfunction")?;
169
170 Ok(())
171}
172
173fn remove_connector(identifier: &str, datapack_dir: impl AsRef<Path>) {
174 let expand_template = |template: &str| expand_template(template, identifier);
175 let datapack_dir = datapack_dir.as_ref();
176
177 macro_rules! remove_from_function_tag {
178 ($relative_path:expr) => {{
179 let path = datapack_dir.join($relative_path);
180 let template = expand_template(include_datapack_template!($relative_path));
181 log_cleanup_error(remove_from_function_tag(path, &template))
182 }};
183 }
184 remove_from_function_tag!("data/minect_internal/tags/functions/connect/choose_chunk.json");
185 remove_from_function_tag!("data/minect_internal/tags/functions/connect/prompt.json");
186
187 let remove = |template_path| {
188 let path = datapack_dir.join(expand_template(template_path));
189 log_cleanup_error(remove_dir_all(path));
190 };
191 remove("data/minect_internal/functions/connection/-connection_id-/connect");
192}
193
194fn create_disconnector(
195 identifier: &str,
196 datapack_dir: impl AsRef<Path>,
197) -> Result<(), IoErrorAtPath> {
198 let expand_template = |template: &str| expand_template(template, identifier);
199 let datapack_dir = datapack_dir.as_ref();
200
201 macro_rules! add_to_function_tag {
202 ($relative_path:expr) => {{
203 let path = datapack_dir.join($relative_path);
204 let template = expand_template(include_datapack_template!($relative_path));
205 add_to_function_tag(path, &template)
206 }};
207 }
208 add_to_function_tag!("data/minect_internal/tags/functions/disconnect/prompt.json")?;
209
210 macro_rules! expand {
211 ($relative_path:expr) => {{
212 let path = datapack_dir.join(expand_template($relative_path));
213 let contents = expand_template(include_datapack_template!($relative_path));
214 write(path, &contents)
215 }};
216 }
217 expand!(
218 "data/minect_internal/functions/connection/-connection_id-/disconnect/prompt.mcfunction"
219 )?;
220
221 Ok(())
222}
223
224fn remove_disconnector(identifier: &str, datapack_dir: impl AsRef<Path>) {
225 let expand_template = |template: &str| expand_template(template, identifier);
226 let datapack_dir = datapack_dir.as_ref();
227
228 macro_rules! remove_from_function_tag {
229 ($relative_path:expr) => {{
230 let path = datapack_dir.join($relative_path);
231 let template = expand_template(include_datapack_template!($relative_path));
232 log_cleanup_error(remove_from_function_tag(path, &template))
233 }};
234 }
235 remove_from_function_tag!("data/minect_internal/tags/functions/disconnect/prompt.json");
236
237 let remove = |template_path| {
238 let path = datapack_dir.join(expand_template(template_path));
239 log_cleanup_error(remove_dir_all(path));
240 };
241 remove("data/minect_internal/functions/connection/-connection_id-/disconnect");
242}
243
244fn remove_empty_dirs(datapack_dir: impl AsRef<Path>) {
245 for entry in WalkDir::new(&datapack_dir).contents_first(true) {
246 if let Ok(entry) = entry {
247 if entry.file_type().is_dir() {
248 let _ = remove_dir(entry.path());
249 }
250 }
251 }
252}
253
254fn log_cleanup_error(result: Result<(), impl Display>) {
256 if let Err(e) = result {
257 error!("Failed to clean up after connect: {}", e)
258 }
259}
260
261fn expand_template(template: &str, identifier: &str) -> String {
262 template.replace("-connection_id-", identifier)
263}
264
265async fn wait_for_connection(connection: &mut MinecraftConnection) -> Result<(), ConnectError> {
266 const CONNECT_OUTPUT_PREFIX: &str = "minect_connect_";
267 const LISTENER_NAME: &str = "minect_connect";
268
269 let events = connection.add_named_listener(LISTENER_NAME);
270
271 connection.execute_commands([Command::named(
272 LISTENER_NAME,
273 summon_named_entity_command(&format!("{}success", CONNECT_OUTPUT_PREFIX)),
274 )])?;
275
276 enum Output {
277 Success,
278 Cancelled,
279 }
280 impl TryFrom<LogEvent> for Output {
281 type Error = ();
282 fn try_from(event: LogEvent) -> Result<Self, Self::Error> {
283 let output = if let Ok(output) = event.output.parse::<SummonNamedEntityOutput>() {
284 output.name
285 } else if let Ok(output) = event.output.parse::<AddTagOutput>() {
286 output.tag
287 } else {
288 return Err(());
289 };
290
291 match output.strip_prefix(CONNECT_OUTPUT_PREFIX) {
292 Some("success") => Ok(Output::Success),
293 Some("cancelled") => Ok(Output::Cancelled),
294 _ => Err(()),
295 }
296 }
297 }
298 let output = events
299 .filter_map(|event| event.try_into().ok())
300 .next()
301 .await;
302 match output.expect("LogObserver panicked") {
303 Output::Success => Ok(()),
304 Output::Cancelled => Err(ConnectError::new(ConnectErrorInner::Cancelled)),
305 }
306}
307
308#[derive(Debug, Deserialize, Serialize)]
309struct FunctionTag {
310 #[serde(default)]
311 values: IndexSet<String>,
312}
313impl FunctionTag {
314 fn new() -> FunctionTag {
315 FunctionTag {
316 values: IndexSet::new(),
317 }
318 }
319}
320
321fn add_to_function_tag(path: impl AsRef<Path>, template: &str) -> Result<(), IoErrorAtPath> {
322 let tag_template: FunctionTag = serde_json::from_str(template).unwrap(); modify_function_tag(path, |tag| {
325 tag.values.extend(tag_template.values);
326 })
327}
328
329fn remove_from_function_tag(path: impl AsRef<Path>, template: &str) -> Result<(), IoErrorAtPath> {
330 let tag_template: FunctionTag = serde_json::from_str(template).unwrap(); modify_function_tag(path, |tag| {
333 tag.values
334 .retain(|value| !tag_template.values.contains(value))
335 })
336}
337
338fn modify_function_tag(
339 path: impl AsRef<Path>,
340 modify: impl FnOnce(&mut FunctionTag),
341) -> Result<(), IoErrorAtPath> {
342 let (mut tag, mut file) = read_function_tag(&path)?;
343 let old_len = tag.values.len();
344 modify(&mut tag);
345 let modified = old_len != tag.values.len();
346 if modified {
347 write_function_tag(&tag, &mut file, path)?;
348 }
349 Ok(())
350}
351
352fn read_function_tag(path: &impl AsRef<Path>) -> Result<(FunctionTag, File), IoErrorAtPath> {
353 if let Some(parent) = path.as_ref().parent() {
354 create_dir_all(parent)?;
355 }
356 let file = OpenOptions::new()
357 .create(true)
358 .read(true)
359 .write(true)
360 .open(path)
361 .map_err(io_error("Failed to open file", path.as_ref()))?;
362 let reader = BufReader::new(&file);
363 let tag = match serde_json::from_reader(reader) {
364 Ok(tag) => tag,
365 Err(e) if e.is_eof() && e.line() == 1 && e.column() == 0 => FunctionTag::new(),
366 Err(e) => return Err(IoErrorAtPath::new("Failed to parse file", path.as_ref(), e)),
367 };
368 Ok((tag, file))
369}
370
371fn write_function_tag(
372 tag: &FunctionTag,
373 file: &mut File,
374 path: impl AsRef<Path>,
375) -> Result<(), IoErrorAtPath> {
376 file.set_len(0)
377 .map_err(io_error("Failed to truncate file", path.as_ref()))?;
378 file.seek(SeekFrom::Start(0))
379 .map_err(io_error("Failed to seek beginning of file", path.as_ref()))?;
380 let mut writer = BufWriter::new(file);
381 serde_json::to_writer_pretty(&mut writer, tag)
382 .map_err(io_error("Failed to write to file", path.as_ref()))
383}