mirror of
https://github.com/Dummi26/mcdcbot.git
synced 2025-03-10 05:13:53 +01:00
initial commit
This commit is contained in:
commit
b2fefc92ee
2
.gitignore
vendored
Executable file
2
.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
*/Cargo.lock
|
||||||
|
*/target/
|
30
README.md
Executable file
30
README.md
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
# mcdcbot
|
||||||
|
|
||||||
|
- shut down / start your minecraft server from discord
|
||||||
|
- switch between multiple different worlds (/servers) from discord
|
||||||
|
- forward messages from a certain discord channel to the minecraft chat, and minecraft chat messages to that discord channel
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
See the repository for examples, notable `mcdcbot/servers/*` and `mcdcbot/settings.txt`.
|
||||||
|
|
||||||
|
For advanced config options, check `minecraft_manager/src/lib.rs`, especially the `fn from_lines()`.
|
||||||
|
|
||||||
|
Documentation may be added in the future...
|
||||||
|
|
||||||
|
### In Discord:
|
||||||
|
|
||||||
|
`/list` lists Servers:
|
||||||
|
|
||||||
|
- (m) My Server
|
||||||
|
- (t) Test World
|
||||||
|
|
||||||
|
`/starts` starts a server. You can choose which one to start.
|
||||||
|
Only one server can be running at any given time.
|
||||||
|
|
||||||
|
`/start m` or `/start My Server`
|
||||||
|
|
||||||
|
`/start t` or `/start Test World`
|
||||||
|
|
||||||
|
`/stop` runs the `stop` command in the current server.
|
||||||
|
Once the server shuts down, a message will be sent.
|
12
mcdcbot/Cargo.toml
Executable file
12
mcdcbot/Cargo.toml
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "mcdcbot"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
minecraft_manager = { path = "../minecraft_manager" }
|
||||||
|
poise = "0.5.7"
|
||||||
|
reqwest = "0.11.22"
|
||||||
|
tokio = { version = "1.34.0", default-features = false }
|
1
mcdcbot/my_token.txt
Executable file
1
mcdcbot/my_token.txt
Executable file
@ -0,0 +1 @@
|
|||||||
|
MTE3NzE5NzAwNDgyOTUxMTcyMA.GShE0Z.EmJ9dyHeaH3-NUsjHwiu5icvIojDn6k6f3hU30
|
4
mcdcbot/servers/MyTest
Executable file
4
mcdcbot/servers/MyTest
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
type=vanilla-mojang
|
||||||
|
dir=/markone/temp/mc/MyTest
|
||||||
|
exec=server.jar
|
||||||
|
ram=2048
|
6
mcdcbot/settings.txt
Executable file
6
mcdcbot/settings.txt
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
channel_id_info=1177200748648464464
|
||||||
|
channel_id_chat=1177200760480612392
|
||||||
|
send_join_and_leave_messages=true
|
||||||
|
send_start_stop_messages_in_chat=true
|
||||||
|
get_my_ip_url1=https://ipinfo.io/ip
|
||||||
|
get_my_ip_url2=https://ipecho.net/plain
|
25
mcdcbot/src/data.rs
Executable file
25
mcdcbot/src/data.rs
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use minecraft_manager::{thread::MinecraftServerThread, MinecraftServerSettings};
|
||||||
|
use poise::futures_util::lock::Mutex;
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub settings: Mutex<Settings>,
|
||||||
|
pub servers: Mutex<Vec<Arc<Mutex<MinecraftServer>>>>,
|
||||||
|
pub current: Arc<
|
||||||
|
Mutex<
|
||||||
|
Option<(
|
||||||
|
Arc<Mutex<MinecraftServer>>,
|
||||||
|
Arc<Mutex<Option<MinecraftServerThread>>>,
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MinecraftServer {
|
||||||
|
pub name: String,
|
||||||
|
pub short: Option<String>,
|
||||||
|
pub settings: MinecraftServerSettings,
|
||||||
|
}
|
55
mcdcbot/src/embed.rs
Executable file
55
mcdcbot/src/embed.rs
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
use minecraft_manager::{
|
||||||
|
chat::ChatMessage, events::JoinLeaveEvent, threaded::MinecraftServerStopReason,
|
||||||
|
};
|
||||||
|
use poise::serenity_prelude::{json::json, json::Value};
|
||||||
|
|
||||||
|
pub fn chat_message(e: &ChatMessage) -> Value {
|
||||||
|
json!({
|
||||||
|
"embeds": [{
|
||||||
|
"title": e.author,
|
||||||
|
"description": e.message
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn join_leave(e: &JoinLeaveEvent) -> Value {
|
||||||
|
json!({
|
||||||
|
"embeds": [{
|
||||||
|
"description": if e.joined {
|
||||||
|
format!("{} joined", e.username)
|
||||||
|
} else {
|
||||||
|
format!("{} left", e.username)
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_started(name: &str, ip: Option<String>) -> Value {
|
||||||
|
json!({
|
||||||
|
"embeds": [{
|
||||||
|
"color": 26880,
|
||||||
|
"title": name,
|
||||||
|
"description": if let Some(ip) = ip {
|
||||||
|
format!("Server was started, IP: {ip}")
|
||||||
|
} else {
|
||||||
|
format!("Server was started")
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn server_stopped(reason: Option<MinecraftServerStopReason>) -> Value {
|
||||||
|
if let Some(reason) = reason {
|
||||||
|
json!({
|
||||||
|
"embeds": [{
|
||||||
|
"color": 6881280,
|
||||||
|
"title": reason.to_string(),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
json!({
|
||||||
|
"embeds": [{
|
||||||
|
"color": 6881280,
|
||||||
|
"title": "Stopped.",
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
31
mcdcbot/src/getmyip.rs
Executable file
31
mcdcbot/src/getmyip.rs
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
pub async fn get_my_ip(url1: &str, url2: &str) -> String {
|
||||||
|
let ip1 = get(url1).await;
|
||||||
|
let ip2 = get(url2).await;
|
||||||
|
match (ip1, ip2) {
|
||||||
|
(Some(ip1), Some(ip2)) => {
|
||||||
|
if ip1 == ip2 {
|
||||||
|
ip1.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{ip1} / {ip2}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(ip), None) | (None, Some(ip)) => ip.to_string(),
|
||||||
|
(None, None) => format!("unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(url: &str) -> Option<String> {
|
||||||
|
if url.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
reqwest::get(url)
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
324
mcdcbot/src/main.rs
Executable file
324
mcdcbot/src/main.rs
Executable file
@ -0,0 +1,324 @@
|
|||||||
|
mod data;
|
||||||
|
mod embed;
|
||||||
|
mod getmyip;
|
||||||
|
mod settings;
|
||||||
|
|
||||||
|
use std::{collections::HashSet, env, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use crate::{data::Data, settings::Settings};
|
||||||
|
use minecraft_manager::{
|
||||||
|
events::{MinecraftServerEventType, MinecraftServerWarning},
|
||||||
|
tasks::MinecraftServerTask,
|
||||||
|
thread::MinecraftServerThread,
|
||||||
|
};
|
||||||
|
use poise::{futures_util::lock::Mutex, serenity_prelude as serenity};
|
||||||
|
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
async fn list(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.say({
|
||||||
|
let mut acc = format!("Available servers:");
|
||||||
|
for server in ctx.data().servers.lock().await.iter() {
|
||||||
|
let server = server.lock().await;
|
||||||
|
acc.push_str("\n- ");
|
||||||
|
if let Some(short) = &server.short {
|
||||||
|
acc.push('(');
|
||||||
|
acc.push_str(short);
|
||||||
|
acc.push_str(") ");
|
||||||
|
}
|
||||||
|
acc.push_str(server.name.as_str());
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
async fn start(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Server's name (see /list)"] srv: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// find server by name
|
||||||
|
let servers_lock = ctx.data().servers.lock().await;
|
||||||
|
let mut matching_server = None;
|
||||||
|
for server in servers_lock.iter() {
|
||||||
|
let server_lock = server.lock().await;
|
||||||
|
if server_lock
|
||||||
|
.short
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|short| short == &srv)
|
||||||
|
{
|
||||||
|
matching_server = Some(Arc::clone(server));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matching_server.is_none() {
|
||||||
|
for server in servers_lock.iter() {
|
||||||
|
let server_lock = server.lock().await;
|
||||||
|
if server_lock.name == srv {
|
||||||
|
matching_server = Some(Arc::clone(server));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(server) = matching_server {
|
||||||
|
let mut current_lock = ctx.data().current.lock().await;
|
||||||
|
if let Some((current, _)) = current_lock.as_ref() {
|
||||||
|
let current = current.lock().await;
|
||||||
|
ctx.say(format!(
|
||||||
|
"Already running '{}'! (stop the server before starting it)",
|
||||||
|
current.name,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let server_lock = server.lock().await;
|
||||||
|
let settings = ctx.data().settings.lock().await;
|
||||||
|
_ = ctx
|
||||||
|
.http()
|
||||||
|
.send_message(
|
||||||
|
settings.channel_id_info,
|
||||||
|
&embed::server_started(
|
||||||
|
&server_lock.name,
|
||||||
|
Some(
|
||||||
|
getmyip::get_my_ip(&settings.get_my_ip_url1, &settings.get_my_ip_url2)
|
||||||
|
.await,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if settings.send_start_stop_messages_in_chat {
|
||||||
|
_ = ctx
|
||||||
|
.http()
|
||||||
|
.send_message(
|
||||||
|
settings.channel_id_chat,
|
||||||
|
&embed::server_started(&server_lock.name, None),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let thread = server_lock.settings.clone().spawn();
|
||||||
|
drop(server_lock);
|
||||||
|
*current_lock = Some((server, Arc::new(Mutex::new(Some(thread)))));
|
||||||
|
ctx.say(format!("Starting...")).await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.say(format!("Can't find a server with that name!"))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
async fn stop(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let current_lock = ctx.data().current.lock().await;
|
||||||
|
if let Some((_, thread)) = current_lock.as_ref() {
|
||||||
|
_ = thread
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.clone_task_sender()
|
||||||
|
.send_task(MinecraftServerTask::Stop);
|
||||||
|
ctx.say(format!("Stopping...")).await?;
|
||||||
|
} else {
|
||||||
|
ctx.say(format!("Use /start to start a server first"))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn event_handler(
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
event: &poise::Event<'_>,
|
||||||
|
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||||
|
data: &Data,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match event {
|
||||||
|
poise::Event::Message { new_message } => {
|
||||||
|
if !new_message.author.bot
|
||||||
|
&& new_message.channel_id.0 == data.settings.lock().await.channel_id_chat
|
||||||
|
{
|
||||||
|
let current_lock = data.current.lock().await;
|
||||||
|
if let Some((_current, thread)) = current_lock.as_ref() {
|
||||||
|
let msg = new_message.content_safe(&ctx.cache);
|
||||||
|
let msg = msg
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
let author = new_message
|
||||||
|
.author_nick(&ctx)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|| new_message.author.name.clone());
|
||||||
|
_ = thread
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.clone_task_sender()
|
||||||
|
.send_task(MinecraftServerTask::RunCommand(format!(
|
||||||
|
"tellraw @a \"<{author}> {msg}\"",
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
// read settings file
|
||||||
|
let settings =
|
||||||
|
Settings::from_file(env::var("McDcBotSettingsFile").unwrap_or(format!("settings.txt")))
|
||||||
|
.unwrap();
|
||||||
|
// read mc servers
|
||||||
|
let mut servers = vec![];
|
||||||
|
for file in std::fs::read_dir(env::var("McDcBotServersDir").unwrap_or(format!("servers")))
|
||||||
|
.expect("Couldn't read servers dir, maybe specify the directory with the McDcBotServersDir env variable?") {
|
||||||
|
let file = file.unwrap();
|
||||||
|
let content = std::fs::read_to_string(file.path()).unwrap();
|
||||||
|
let mut lines = content.lines().filter(|line| !line.trim().is_empty());
|
||||||
|
let settings = minecraft_manager::MinecraftServerSettings::from_lines(&mut lines).unwrap();
|
||||||
|
servers.push(data::MinecraftServer {
|
||||||
|
name: file.file_name().to_string_lossy().into_owned(),
|
||||||
|
short: None,
|
||||||
|
settings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut shorts = HashSet::new();
|
||||||
|
for server in &mut servers {
|
||||||
|
if let Some(ch) = server.name.trim().chars().next() {
|
||||||
|
let ch = ch.to_lowercase();
|
||||||
|
if shorts.insert(format!("{ch}")) {
|
||||||
|
server.short = Some(format!("{ch}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let initials = server
|
||||||
|
.name
|
||||||
|
.trim()
|
||||||
|
.split_whitespace()
|
||||||
|
.filter_map(|v| v.chars().next().map(|c| c.to_uppercase()))
|
||||||
|
.flatten()
|
||||||
|
.collect::<String>();
|
||||||
|
if !initials.is_empty() {
|
||||||
|
if shorts.insert(initials.clone()) {
|
||||||
|
server.short = Some(initials);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// current server
|
||||||
|
let current = Arc::new(Mutex::new(None));
|
||||||
|
let current_thread = Arc::clone(¤t);
|
||||||
|
// start
|
||||||
|
let framework = poise::Framework::builder()
|
||||||
|
.options(poise::FrameworkOptions {
|
||||||
|
commands: vec![list(), start(), stop()],
|
||||||
|
event_handler: |ctx, event, framework, data| {
|
||||||
|
Box::pin(event_handler(ctx, event, framework, data))
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.token(env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN env var"))
|
||||||
|
.intents(
|
||||||
|
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT,
|
||||||
|
)
|
||||||
|
.setup(|ctx, ready, framework: &poise::Framework<Data, Error>| {
|
||||||
|
Box::pin(async move {
|
||||||
|
ctx.idle().await;
|
||||||
|
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||||
|
eprintln!("Connected as '{}'.", ready.user.name);
|
||||||
|
{
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let settings = settings.clone();
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let sleep_time = Duration::from_millis(100);
|
||||||
|
let mut running = false;
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(sleep_time).await;
|
||||||
|
let mut current_lock: poise::futures_util::lock::MutexGuard<
|
||||||
|
'_,
|
||||||
|
Option<(
|
||||||
|
Arc<Mutex<data::MinecraftServer>>,
|
||||||
|
Arc<Mutex<Option<MinecraftServerThread>>>,
|
||||||
|
)>,
|
||||||
|
> = current_thread.lock().await;
|
||||||
|
if let Some((_current_server, current_thread_mutex)) =
|
||||||
|
current_lock.as_ref()
|
||||||
|
{
|
||||||
|
let mut current_thread_opt = current_thread_mutex.lock().await;
|
||||||
|
let current_thread = current_thread_opt.as_mut().unwrap();
|
||||||
|
current_thread.update();
|
||||||
|
for event in current_thread.handle_new_events() {
|
||||||
|
match &event.event {
|
||||||
|
MinecraftServerEventType::Warning(w) => match w {
|
||||||
|
MinecraftServerWarning::CouldNotGetServerProcessStdio
|
||||||
|
| MinecraftServerWarning::CantWriteToStdin(_) => {
|
||||||
|
ctx.dnd().await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MinecraftServerEventType::JoinLeave(e) => {
|
||||||
|
if settings.send_join_and_leave_messages {
|
||||||
|
_ = ctx
|
||||||
|
.http
|
||||||
|
.send_message(
|
||||||
|
settings.channel_id_chat,
|
||||||
|
&embed::join_leave(
|
||||||
|
e
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MinecraftServerEventType::ChatMessage(e) => {
|
||||||
|
_ = ctx
|
||||||
|
.http
|
||||||
|
.send_message(
|
||||||
|
settings.channel_id_chat,
|
||||||
|
&embed::chat_message(e),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current_thread.is_finished() {
|
||||||
|
let cto = current_thread_opt.take().unwrap();
|
||||||
|
let msg = embed::server_stopped(cto.get_stop_reason().ok());
|
||||||
|
_ = ctx.http.send_message(settings.channel_id_info, &msg).await;
|
||||||
|
if settings.send_start_stop_messages_in_chat {
|
||||||
|
_ = ctx
|
||||||
|
.http
|
||||||
|
.send_message(settings.channel_id_chat, &msg)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
running = false;
|
||||||
|
drop(current_thread_opt);
|
||||||
|
*current_lock = None;
|
||||||
|
ctx.idle().await;
|
||||||
|
} else if !running {
|
||||||
|
running = true;
|
||||||
|
ctx.online().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Data {
|
||||||
|
settings: Mutex::new(settings),
|
||||||
|
current,
|
||||||
|
servers: Mutex::new(
|
||||||
|
servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| Arc::new(Mutex::new(s)))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
framework.run().await.unwrap();
|
||||||
|
}
|
51
mcdcbot/src/settings.rs
Executable file
51
mcdcbot/src/settings.rs
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub channel_id_info: u64,
|
||||||
|
pub channel_id_chat: u64,
|
||||||
|
pub send_join_and_leave_messages: bool,
|
||||||
|
pub send_start_stop_messages_in_chat: bool,
|
||||||
|
pub get_my_ip_url1: String,
|
||||||
|
pub get_my_ip_url2: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub fn from_file(path: impl AsRef<Path>) -> std::io::Result<Self> {
|
||||||
|
let file = std::fs::read_to_string(path)?;
|
||||||
|
let mut cii = None;
|
||||||
|
let mut cic = None;
|
||||||
|
let mut send_join_and_leave_messages = false;
|
||||||
|
let mut send_start_stop_messages_in_chat = false;
|
||||||
|
let mut get_my_ip_url1 = String::new();
|
||||||
|
let mut get_my_ip_url2 = String::new();
|
||||||
|
for (name, value) in file
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.split_once("=").unwrap_or((line, "")))
|
||||||
|
{
|
||||||
|
match name {
|
||||||
|
"channel_id_info" => {
|
||||||
|
cii = value.trim().parse().ok();
|
||||||
|
}
|
||||||
|
"channel_id_chat" => {
|
||||||
|
cic = value.trim().parse().ok();
|
||||||
|
}
|
||||||
|
"get_my_ip_url1" => get_my_ip_url1 = value.trim().to_owned(),
|
||||||
|
"get_my_ip_url2" => get_my_ip_url2 = value.trim().to_owned(),
|
||||||
|
"send_join_and_leave_messages" => send_join_and_leave_messages = value != "false",
|
||||||
|
"send_start_stop_messages_in_chat" => {
|
||||||
|
send_start_stop_messages_in_chat = value != "false"
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
channel_id_info: cii.expect("[settings] Missing `channel_id_info`"),
|
||||||
|
channel_id_chat: cic.expect("[settings] Missing `channel_id_chat`"),
|
||||||
|
send_join_and_leave_messages,
|
||||||
|
send_start_stop_messages_in_chat,
|
||||||
|
get_my_ip_url1,
|
||||||
|
get_my_ip_url2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
8
minecraft_manager/Cargo.toml
Executable file
8
minecraft_manager/Cargo.toml
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "minecraft_manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
5
minecraft_manager/src/chat.rs
Executable file
5
minecraft_manager/src/chat.rs
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChatMessage {
|
||||||
|
pub author: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
27
minecraft_manager/src/events.rs
Executable file
27
minecraft_manager/src/events.rs
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
use crate::chat::ChatMessage;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MinecraftServerEvent {
|
||||||
|
pub time: (),
|
||||||
|
pub event: MinecraftServerEventType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MinecraftServerEventType {
|
||||||
|
Warning(MinecraftServerWarning),
|
||||||
|
JoinLeave(JoinLeaveEvent),
|
||||||
|
ChatMessage(ChatMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MinecraftServerWarning {
|
||||||
|
/// The server process was spawned, but std{in,out,err} was not captured.
|
||||||
|
CouldNotGetServerProcessStdio,
|
||||||
|
CantWriteToStdin(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct JoinLeaveEvent {
|
||||||
|
pub username: String,
|
||||||
|
pub joined: bool,
|
||||||
|
}
|
295
minecraft_manager/src/lib.rs
Executable file
295
minecraft_manager/src/lib.rs
Executable file
@ -0,0 +1,295 @@
|
|||||||
|
pub mod chat;
|
||||||
|
pub mod events;
|
||||||
|
pub mod parse_line;
|
||||||
|
pub mod tasks;
|
||||||
|
pub mod thread;
|
||||||
|
pub mod threaded;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
io::BufReader,
|
||||||
|
process::{Child, ChildStdin, ChildStdout, Command},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use thread::MinecraftServerThread;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MinecraftServerSettings {
|
||||||
|
pub server_type: MinecraftServerType,
|
||||||
|
pub directory: String,
|
||||||
|
pub executable: String,
|
||||||
|
/// the amount of dedicated wam for the JVM in [TODO!] (-Xm{s,x}...M)
|
||||||
|
pub dedicated_wam: u32,
|
||||||
|
pub java_cmd: Option<String>,
|
||||||
|
}
|
||||||
|
impl Display for MinecraftServerSettings {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} @ {} :: {} @ {}MB",
|
||||||
|
self.server_type, self.directory, self.executable, self.dedicated_wam
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MinecraftServerSettings {
|
||||||
|
/// takes lines from the provided iterator until an empty line is reached (line.trim().is_empty()) or the iterator ends.
|
||||||
|
/// Note: The iterator items should NOT contain newline characters!
|
||||||
|
pub fn from_lines<'a, L: Iterator<Item = &'a str>>(
|
||||||
|
lines: &mut L,
|
||||||
|
) -> Result<Self, MinecraftServerSettingsFromLinesError> {
|
||||||
|
let mut server_type = Err(MinecraftServerSettingsFromLinesError::MissingServerType);
|
||||||
|
let mut directory = Err(MinecraftServerSettingsFromLinesError::MissingDirectory);
|
||||||
|
let mut executable = Err(MinecraftServerSettingsFromLinesError::MissingExecutable);
|
||||||
|
let mut ram = None;
|
||||||
|
let mut java_cmd = None;
|
||||||
|
let mut extra_line = None;
|
||||||
|
loop {
|
||||||
|
if let Some(line) = if let Some(l) = extra_line.take() {
|
||||||
|
Some(l)
|
||||||
|
} else {
|
||||||
|
lines.next()
|
||||||
|
} {
|
||||||
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
|
match key {
|
||||||
|
"type" => {
|
||||||
|
server_type = Ok(match value.trim() {
|
||||||
|
"vanilla-mojang" => MinecraftServerType::VanillaMojang,
|
||||||
|
"vanilla-papermc" => MinecraftServerType::VanillaPaperMC,
|
||||||
|
"custom" => {
|
||||||
|
let mut name = Err(MinecraftServerSettingsFromLinesError::CustomServerTypeMissingName);
|
||||||
|
let mut line_parser = Err(MinecraftServerSettingsFromLinesError::CustomServerTypeMissingLineParser);
|
||||||
|
let mut command_override = None;
|
||||||
|
loop {
|
||||||
|
if let Some(line) = lines.next() {
|
||||||
|
if let Some(c) = line.chars().next() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
if let Some((key, val)) =
|
||||||
|
line.trim_start().split_once('=')
|
||||||
|
{
|
||||||
|
match key {
|
||||||
|
"name" => name = Ok(val.to_owned()),
|
||||||
|
"parser" => line_parser = Ok(val.to_owned()),
|
||||||
|
"command-override" => command_override = Some(val.to_owned()),
|
||||||
|
_ =>
|
||||||
|
return Err(MinecraftServerSettingsFromLinesError::CustomTypeUnknownKey(
|
||||||
|
key.to_owned()
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(MinecraftServerSettingsFromLinesError::CustomTypeUnknownKey(
|
||||||
|
line.trim_start().to_owned()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extra_line = Some(line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MinecraftServerType::Custom {
|
||||||
|
name: name?,
|
||||||
|
line_parser: line_parser?,
|
||||||
|
line_parser_proc: Arc::new(Mutex::new(None)),
|
||||||
|
command_override,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(
|
||||||
|
MinecraftServerSettingsFromLinesError::UnknownServerType(
|
||||||
|
other.to_owned(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"dir" => directory = Ok(value.to_owned()),
|
||||||
|
"exec" => executable = Ok(value.to_owned()),
|
||||||
|
"ram" => {
|
||||||
|
if let Ok(v) = value.trim().parse() {
|
||||||
|
ram = Some(v);
|
||||||
|
} else {
|
||||||
|
return Err(MinecraftServerSettingsFromLinesError::RamNotAnInt(
|
||||||
|
value.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"java_cmd" => java_cmd = Some(value.to_owned()),
|
||||||
|
k => {
|
||||||
|
return Err(MinecraftServerSettingsFromLinesError::UnknownKey(
|
||||||
|
k.to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line.trim().is_empty() {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return Err(MinecraftServerSettingsFromLinesError::UnknownKey(
|
||||||
|
line.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut o = Self::new(server_type?, directory?, executable?);
|
||||||
|
if let Some(ram) = ram {
|
||||||
|
o = o.with_ram(ram);
|
||||||
|
}
|
||||||
|
if let Some(java_cmd) = java_cmd {
|
||||||
|
o = o.with_java_cmd(Some(java_cmd));
|
||||||
|
}
|
||||||
|
Ok(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MinecraftServerSettingsFromLinesError {
|
||||||
|
UnknownKey(String),
|
||||||
|
MissingServerType,
|
||||||
|
UnknownServerType(String),
|
||||||
|
MissingDirectory,
|
||||||
|
MissingExecutable,
|
||||||
|
RamNotAnInt(String),
|
||||||
|
CustomTypeUnknownKey(String),
|
||||||
|
CustomServerTypeMissingName,
|
||||||
|
CustomServerTypeMissingLineParser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftServerSettings {
|
||||||
|
pub fn spawn(self) -> MinecraftServerThread {
|
||||||
|
MinecraftServerThread::start(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(server_type: MinecraftServerType, directory: String, executable: String) -> Self {
|
||||||
|
Self {
|
||||||
|
server_type,
|
||||||
|
directory,
|
||||||
|
executable,
|
||||||
|
dedicated_wam: 1024,
|
||||||
|
java_cmd: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_ram(mut self, ram_mb: u32) -> Self {
|
||||||
|
self.dedicated_wam = ram_mb;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn with_java_cmd(mut self, java_cmd: Option<String>) -> Self {
|
||||||
|
self.java_cmd = java_cmd;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_command(&self) -> Command {
|
||||||
|
let mut cmd = Command::new(if let Some(c) = &self.java_cmd {
|
||||||
|
c.as_str()
|
||||||
|
} else {
|
||||||
|
match &self.server_type {
|
||||||
|
MinecraftServerType::VanillaMojang => "java", // "/usr/lib/jvm/openjdk17/bin/java",
|
||||||
|
MinecraftServerType::VanillaPaperMC => "java", // "/usr/lib/jvm/openjdk17/bin/java",
|
||||||
|
MinecraftServerType::Custom {
|
||||||
|
command_override, ..
|
||||||
|
} => {
|
||||||
|
if let Some(cmd) = command_override {
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
"java"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cmd.current_dir(&self.directory);
|
||||||
|
// match &self.server_type {
|
||||||
|
// MinecraftServerType::VanillaMojang | MinecraftServerType::VanillaPaperMC =>
|
||||||
|
cmd.args([
|
||||||
|
format!("-Xms{}M", self.dedicated_wam),
|
||||||
|
format!("-Xmx{}M", self.dedicated_wam),
|
||||||
|
"-Dsun.stdout.encoding=UTF-8".to_owned(),
|
||||||
|
"-Dsun.stderr.encoding=UTF-8".to_owned(),
|
||||||
|
"-DFile.Encoding=UTF-8".to_owned(),
|
||||||
|
"-jar".to_string(),
|
||||||
|
self.executable.to_string(),
|
||||||
|
"nogui".to_string(),
|
||||||
|
]);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum MinecraftServerType {
|
||||||
|
VanillaMojang,
|
||||||
|
VanillaPaperMC,
|
||||||
|
Custom {
|
||||||
|
/// your custom server type's name
|
||||||
|
name: String,
|
||||||
|
/// each time a line is received from the mc server's stdout, it is sent to this programs stdin.
|
||||||
|
/// if the program has terminated, it is started again.
|
||||||
|
/// for best performance, the program should read stdin lines in a loop and never exit
|
||||||
|
line_parser: String,
|
||||||
|
line_parser_proc: Arc<Mutex<Option<(Child, ChildStdin, BufReader<ChildStdout>)>>>,
|
||||||
|
/// instead of running java -jar [...], use this to run a shell script which then starts the server.
|
||||||
|
/// things like ram etc will be ignored if this is used.
|
||||||
|
command_override: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
impl Display for MinecraftServerType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::VanillaMojang => write!(f, "vanilla-mojang"),
|
||||||
|
Self::VanillaPaperMC => write!(f, "vanilla-papermc"),
|
||||||
|
Self::Custom {
|
||||||
|
name: identifier, ..
|
||||||
|
} => write!(f, "custom ({identifier})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test() {
|
||||||
|
// create minecraft server config
|
||||||
|
let minecraft_server_settings = MinecraftServerSettings {
|
||||||
|
server_type: MinecraftServerType::VanillaPaperMC,
|
||||||
|
directory: "/home/mark/Dokumente/minecraft_server/1".to_string(),
|
||||||
|
executable: "paper-1.19-81.jar".to_string(),
|
||||||
|
dedicated_wam: 1024,
|
||||||
|
java_cmd: None,
|
||||||
|
};
|
||||||
|
// start server
|
||||||
|
let mut thread = minecraft_server_settings.spawn();
|
||||||
|
// handle stdin
|
||||||
|
if false {
|
||||||
|
let sender = thread.clone_task_sender();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
if let Ok(_) = stdin.read_line(&mut line) {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(300));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Err(_) = sender.send_task(tasks::MinecraftServerTask::RunCommand(line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// handle stdout
|
||||||
|
loop {
|
||||||
|
if !thread.is_finished() {
|
||||||
|
thread.update();
|
||||||
|
for event in thread.handle_new_events() {
|
||||||
|
eprintln!("Event: {event:?}");
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
} else {
|
||||||
|
if let Ok(stop_reason) = thread.get_stop_reason() {
|
||||||
|
eprintln!("Thread stopped: {stop_reason}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
214
minecraft_manager/src/parse_line.rs
Executable file
214
minecraft_manager/src/parse_line.rs
Executable file
@ -0,0 +1,214 @@
|
|||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader, Write},
|
||||||
|
process::{self, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
chat::ChatMessage,
|
||||||
|
events::{self, MinecraftServerEventType},
|
||||||
|
MinecraftServerSettings, MinecraftServerType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ParseOutput {
|
||||||
|
Nothing,
|
||||||
|
Error(ParseError),
|
||||||
|
Event(MinecraftServerEventType),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ParseError {
|
||||||
|
/// any other errors (for custom line parser implementations)
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_line(line: &str, settings: &MinecraftServerSettings) -> ParseOutput {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
return ParseOutput::Nothing;
|
||||||
|
}
|
||||||
|
match &settings.server_type {
|
||||||
|
MinecraftServerType::Custom {
|
||||||
|
line_parser,
|
||||||
|
line_parser_proc,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut proc = line_parser_proc.lock().unwrap();
|
||||||
|
let proc = &mut *proc;
|
||||||
|
let make_new_proc = if let Some((proc, _, _)) = proc {
|
||||||
|
if let Ok(Some(_)) = proc.try_wait() {
|
||||||
|
// has exited
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
if make_new_proc {
|
||||||
|
if let Ok(mut new_proc) = process::Command::new(line_parser)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
if let (Some(stdin), Some(stdout)) =
|
||||||
|
(new_proc.stdin.take(), new_proc.stdout.take())
|
||||||
|
{
|
||||||
|
*proc = Some((new_proc, stdin, BufReader::new(stdout)));
|
||||||
|
} else {
|
||||||
|
eprintln!("[WARN/CUSTOM-LINE-PARSER] No stdin/stdout handles!");
|
||||||
|
_ = new_proc.kill();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't spawn command '{line_parser}'!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((_proc, stdin, stdout)) = proc {
|
||||||
|
if let Err(e) = writeln!(stdin, "{line}") {
|
||||||
|
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't write to stdin: {e:?}");
|
||||||
|
return ParseOutput::Nothing;
|
||||||
|
};
|
||||||
|
let mut buf = String::new();
|
||||||
|
if let Err(e) = stdout.read_line(&mut buf) {
|
||||||
|
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't read_line: {e:?}");
|
||||||
|
return ParseOutput::Nothing;
|
||||||
|
};
|
||||||
|
if buf.ends_with('\n') || buf.ends_with('\r') {
|
||||||
|
buf.pop();
|
||||||
|
}
|
||||||
|
if buf.len() > 0 {
|
||||||
|
match buf.as_bytes()[0] {
|
||||||
|
b'c' => {
|
||||||
|
ParseOutput::Event(MinecraftServerEventType::ChatMessage(ChatMessage {
|
||||||
|
author: buf[1..].to_owned(),
|
||||||
|
message: {
|
||||||
|
let mut o = String::new();
|
||||||
|
if let Err(e) = stdout.read_line(&mut o) {
|
||||||
|
eprintln!(
|
||||||
|
"[WARN/CUSTOM-LINE-PARSER] Can't read_line: {e:?}"
|
||||||
|
);
|
||||||
|
return ParseOutput::Nothing;
|
||||||
|
}
|
||||||
|
o
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
b'j' => ParseOutput::Event(MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: buf[1..].to_owned(),
|
||||||
|
joined: true,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
b'l' => ParseOutput::Event(MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: buf[1..].to_owned(),
|
||||||
|
joined: false,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
b'e' => ParseOutput::Error({
|
||||||
|
if buf.len() > 1 {
|
||||||
|
match buf.as_bytes()[1] {
|
||||||
|
b'c' => ParseError::Custom(buf[2..].to_string()),
|
||||||
|
_ => ParseError::Custom(String::new()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParseError::Custom(String::new())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => ParseOutput::Nothing,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParseOutput::Nothing
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[WARN/CUSTOM-LINE-PARSER] No process!");
|
||||||
|
ParseOutput::Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MinecraftServerType::VanillaMojang => {
|
||||||
|
if let Some((_time, rest)) = line[1..].split_once("] [Server thread/INFO]: ") {
|
||||||
|
let rest = rest.trim();
|
||||||
|
if rest.starts_with("<") {
|
||||||
|
if let Some((user, msg)) = rest[1..].split_once("> ") {
|
||||||
|
return ParseOutput::Event(MinecraftServerEventType::ChatMessage(
|
||||||
|
ChatMessage {
|
||||||
|
author: user.to_owned(),
|
||||||
|
message: msg.to_owned(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if rest.ends_with(" joined the game") {
|
||||||
|
return ParseOutput::Event(MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: rest[0..rest.len() - " joined the game".len()].to_owned(),
|
||||||
|
joined: true,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
} else if rest.ends_with(" left the game") {
|
||||||
|
return ParseOutput::Event(MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: rest[0..rest.len() - " left the game".len()].to_owned(),
|
||||||
|
joined: false,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParseOutput::Nothing
|
||||||
|
// Vanilla servers not yet supported...
|
||||||
|
}
|
||||||
|
MinecraftServerType::VanillaPaperMC => {
|
||||||
|
match line.chars().next() {
|
||||||
|
Some('[') => {
|
||||||
|
if let Some((_time, rest)) = line[1..].split_once(' ') {
|
||||||
|
if let Some((severity, rest)) = rest.split_once(']') {
|
||||||
|
if rest.starts_with(": ") {
|
||||||
|
let rest = &rest[2..];
|
||||||
|
// eprintln!("Time: '{time}', Severity: '{severity}', Rest: '{rest}'.");
|
||||||
|
match severity {
|
||||||
|
"INFO" => {
|
||||||
|
if let Some('<') = rest.chars().next() {
|
||||||
|
if let Some((username, message)) =
|
||||||
|
rest[1..].split_once('>')
|
||||||
|
{
|
||||||
|
return ParseOutput::Event(
|
||||||
|
MinecraftServerEventType::ChatMessage(
|
||||||
|
ChatMessage {
|
||||||
|
author: username.to_string(),
|
||||||
|
message: message[1..].to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} // join/leave
|
||||||
|
if rest.trim_end().ends_with(" joined the game") {
|
||||||
|
let username = &rest[..rest.len() - 16];
|
||||||
|
return ParseOutput::Event(
|
||||||
|
MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: username.to_string(),
|
||||||
|
joined: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rest.trim_end().ends_with(" left the game") {
|
||||||
|
let username = &rest[..rest.len() - 14];
|
||||||
|
return ParseOutput::Event(
|
||||||
|
MinecraftServerEventType::JoinLeave(
|
||||||
|
events::JoinLeaveEvent {
|
||||||
|
username: username.to_string(),
|
||||||
|
joined: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
ParseOutput::Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
minecraft_manager/src/tasks.rs
Executable file
37
minecraft_manager/src/tasks.rs
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum MinecraftServerTask {
|
||||||
|
Stop,
|
||||||
|
Kill,
|
||||||
|
RunCommand(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftServerTask {
|
||||||
|
pub fn generate_callback(
|
||||||
|
self,
|
||||||
|
) -> (
|
||||||
|
(Self, mpsc::Sender<Result<u8, String>>),
|
||||||
|
MinecraftServerTaskCallback,
|
||||||
|
) {
|
||||||
|
let (sender, update_receiver) = mpsc::channel();
|
||||||
|
(
|
||||||
|
(self, sender),
|
||||||
|
MinecraftServerTaskCallback::new(update_receiver),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MinecraftServerTaskCallback {
|
||||||
|
/// Ok(n) if n < 100 = progress in %
|
||||||
|
/// Ok(100) = finished
|
||||||
|
/// Ok(n) if n > 100 = task ended with non-standard exit status (advise checking log)
|
||||||
|
/// Err(_) = custom message (for log)
|
||||||
|
pub recv: mpsc::Receiver<Result<u8, String>>, // TODO: NOT PUBLIC
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftServerTaskCallback {
|
||||||
|
pub fn new(recv: mpsc::Receiver<Result<u8, String>>) -> Self {
|
||||||
|
Self { recv }
|
||||||
|
}
|
||||||
|
}
|
109
minecraft_manager/src/thread.rs
Executable file
109
minecraft_manager/src/thread.rs
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
use std::thread::JoinHandle;
|
||||||
|
|
||||||
|
use crate::tasks::MinecraftServerTaskCallback;
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
events::MinecraftServerEvent,
|
||||||
|
tasks::MinecraftServerTask,
|
||||||
|
threaded::{self, MinecraftServerStopReason},
|
||||||
|
MinecraftServerSettings,
|
||||||
|
},
|
||||||
|
std::{collections::VecDeque, sync::mpsc},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct MinecraftServerThread {
|
||||||
|
events: ThreadData<MinecraftServerEvent>,
|
||||||
|
task_sender: MinecraftServerTaskSender,
|
||||||
|
join_handle: JoinHandle<MinecraftServerStopReason>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clonable type allowing multiple threads to send tasks to the server.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MinecraftServerTaskSender(
|
||||||
|
mpsc::Sender<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MinecraftServerTaskSender {
|
||||||
|
pub fn send_task(&self, task: MinecraftServerTask) -> Result<MinecraftServerTaskCallback, ()> {
|
||||||
|
let (sendable, callback) = task.generate_callback();
|
||||||
|
if let Ok(_) = self.0.send(sendable) {
|
||||||
|
Ok(callback)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftServerThread {
|
||||||
|
pub fn start(settings: MinecraftServerSettings) -> Self {
|
||||||
|
let (task_sender, event_receiver, join_handle) = threaded::run(settings);
|
||||||
|
Self {
|
||||||
|
events: ThreadData::new(event_receiver, 100),
|
||||||
|
task_sender: MinecraftServerTaskSender(task_sender),
|
||||||
|
join_handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_finished(&self) -> bool {
|
||||||
|
self.join_handle.is_finished()
|
||||||
|
}
|
||||||
|
pub fn get_stop_reason(self) -> Result<MinecraftServerStopReason, ()> {
|
||||||
|
if self.is_finished() {
|
||||||
|
if let Ok(v) = self.join_handle.join() {
|
||||||
|
Ok(v)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(&mut self) {
|
||||||
|
self.events.update();
|
||||||
|
}
|
||||||
|
pub fn handle_new_events(
|
||||||
|
&mut self,
|
||||||
|
) -> std::iter::Skip<std::collections::vec_deque::Iter<MinecraftServerEvent>> {
|
||||||
|
self.events.handle_all()
|
||||||
|
}
|
||||||
|
pub fn clone_task_sender(&self) -> MinecraftServerTaskSender {
|
||||||
|
self.task_sender.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreadData<T> {
|
||||||
|
mpsc: mpsc::Receiver<T>,
|
||||||
|
buffer: VecDeque<T>,
|
||||||
|
unhandeled: usize,
|
||||||
|
capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ThreadData<T> {
|
||||||
|
pub fn new(mpsc_receiver: mpsc::Receiver<T>, capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
mpsc: mpsc_receiver,
|
||||||
|
buffer: VecDeque::with_capacity(capacity),
|
||||||
|
unhandeled: 0,
|
||||||
|
capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(&mut self) -> usize {
|
||||||
|
let mut unhandeled = 0;
|
||||||
|
while let Ok(new_content) = self.mpsc.try_recv() {
|
||||||
|
if self.buffer.len() == self.capacity {
|
||||||
|
self.buffer.pop_front();
|
||||||
|
}
|
||||||
|
self.buffer.push_back(new_content);
|
||||||
|
unhandeled += 1;
|
||||||
|
}
|
||||||
|
self.unhandeled += unhandeled;
|
||||||
|
unhandeled
|
||||||
|
}
|
||||||
|
pub fn handle_all(&mut self) -> std::iter::Skip<std::collections::vec_deque::Iter<T>> {
|
||||||
|
let unhandeled = self.unhandeled;
|
||||||
|
self.unhandeled = 0;
|
||||||
|
self.buffer
|
||||||
|
.iter()
|
||||||
|
.skip(self.buffer.len().saturating_sub(unhandeled))
|
||||||
|
}
|
||||||
|
}
|
235
minecraft_manager/src/threaded.rs
Executable file
235
minecraft_manager/src/threaded.rs
Executable file
@ -0,0 +1,235 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
io::{BufRead, BufReader, Write},
|
||||||
|
process::{ExitStatus, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parse_line::{parse_line, ParseOutput},
|
||||||
|
MinecraftServerType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::tasks::MinecraftServerTask,
|
||||||
|
crate::{
|
||||||
|
events::{self as MinecraftServerEvents, MinecraftServerEvent, MinecraftServerEventType},
|
||||||
|
MinecraftServerSettings,
|
||||||
|
},
|
||||||
|
std::sync::mpsc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
settings: MinecraftServerSettings,
|
||||||
|
) -> (
|
||||||
|
mpsc::Sender<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>,
|
||||||
|
mpsc::Receiver<MinecraftServerEvent>,
|
||||||
|
std::thread::JoinHandle<MinecraftServerStopReason>,
|
||||||
|
) {
|
||||||
|
let (return_task_sender, tasks) =
|
||||||
|
mpsc::channel::<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>();
|
||||||
|
let (events, return_events_receiver) = mpsc::channel();
|
||||||
|
|
||||||
|
// thread
|
||||||
|
let join_handle = std::thread::spawn(move || {
|
||||||
|
let mut command = settings.get_command();
|
||||||
|
command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
eprintln!("Spawning {command:?}");
|
||||||
|
match command.spawn() {
|
||||||
|
Ok(mut process) => {
|
||||||
|
if let (Some(mut stdin), Some(stdout), Some(mut _stderr)) = (
|
||||||
|
process.stdin.take(),
|
||||||
|
process.stdout.take(),
|
||||||
|
process.stderr.take(),
|
||||||
|
) {
|
||||||
|
let stdout_lines = {
|
||||||
|
// the stdout reading thread
|
||||||
|
let (lines, stdout_lines) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut stdout = BufReader::new(stdout);
|
||||||
|
let mut line = String::new();
|
||||||
|
loop {
|
||||||
|
line.clear();
|
||||||
|
match stdout.read_line(&mut line) {
|
||||||
|
Ok(_) if !line.trim().is_empty() => {
|
||||||
|
eprintln!("> {}", line.trim());
|
||||||
|
match lines.send(line.trim().to_owned()) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(0) => {
|
||||||
|
eprintln!(
|
||||||
|
" [ Stdout read thread ] Reached EOF, stopping."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(_) => {} // empty line, but read newline char - ignore
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
" [ Stdout read thread ] Read error, stopping. ({e:?})"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stdout_lines
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
while let Ok(task) = tasks.try_recv() {
|
||||||
|
eprintln!("[GOT TASK] {task:?}");
|
||||||
|
// iterate over all new tasks
|
||||||
|
match task.0 {
|
||||||
|
MinecraftServerTask::Stop => match writeln!(stdin, "stop") {
|
||||||
|
Ok(_) => {
|
||||||
|
task.1.send(Ok(0));
|
||||||
|
while let Ok(None) = process.try_wait() {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(
|
||||||
|
250,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
task.1.send(Ok(100));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
events.send(MinecraftServerEvent {
|
||||||
|
time: (),
|
||||||
|
event: MinecraftServerEventType::Warning(
|
||||||
|
MinecraftServerEvents::MinecraftServerWarning::CantWriteToStdin(e),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MinecraftServerTask::Kill => {
|
||||||
|
process.kill();
|
||||||
|
task.1.send(Ok(100));
|
||||||
|
return MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::KilledDueToTask,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
MinecraftServerTask::RunCommand(command) => {
|
||||||
|
match writeln!(
|
||||||
|
stdin,
|
||||||
|
"{}",
|
||||||
|
command.replace("\n", "\\n").replace("\r", "\\r")
|
||||||
|
) {
|
||||||
|
Ok(_) => task.1.send(Ok(100)),
|
||||||
|
Err(_) => task.1.send(Ok(101)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while let Ok(line) = stdout_lines.try_recv() {
|
||||||
|
// iterate over all new lines from stdout
|
||||||
|
// eprintln!(" [ server manager thread ] Found line '{}'", line);
|
||||||
|
match parse_line(&line, &settings) {
|
||||||
|
ParseOutput::Event(event) => {
|
||||||
|
events.send(MinecraftServerEvent { time: (), event });
|
||||||
|
}
|
||||||
|
ParseOutput::Error(_) => (),
|
||||||
|
ParseOutput::Nothing => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stop the loop once the process exits
|
||||||
|
match process.try_wait() {
|
||||||
|
Ok(None) => (),
|
||||||
|
Ok(Some(exit_status)) => {
|
||||||
|
if let MinecraftServerType::Custom {
|
||||||
|
line_parser_proc, ..
|
||||||
|
} = &settings.server_type
|
||||||
|
{
|
||||||
|
if let Some(proc) = &mut *line_parser_proc.lock().unwrap() {
|
||||||
|
_ = proc.0.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::ProcessEnded(exit_status),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::ProcessCouldNotBeAwaited(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("No stdin/out!");
|
||||||
|
events.send(MinecraftServerEvent {
|
||||||
|
time: (),
|
||||||
|
event: MinecraftServerEventType::Warning(
|
||||||
|
MinecraftServerEvents::MinecraftServerWarning::CouldNotGetServerProcessStdio,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
match process.wait() {
|
||||||
|
Ok(status) => MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::ProcessEnded(status),
|
||||||
|
},
|
||||||
|
Err(e) => MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::ProcessCouldNotBeAwaited(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Couldn't spawn server process: {e:?}");
|
||||||
|
MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons::ProcessCouldNotBeSpawned(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// return the mpsc channel parts
|
||||||
|
(return_task_sender, return_events_receiver, join_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MinecraftServerStopReason {
|
||||||
|
time: (),
|
||||||
|
reason: MinecraftServerStopReasons,
|
||||||
|
}
|
||||||
|
impl Display for MinecraftServerStopReason {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MinecraftServerStopReasons {
|
||||||
|
KilledDueToTask,
|
||||||
|
ProcessEnded(ExitStatus),
|
||||||
|
ProcessCouldNotBeSpawned(std::io::Error),
|
||||||
|
ProcessCouldNotBeAwaited(std::io::Error),
|
||||||
|
}
|
||||||
|
impl Display for MinecraftServerStopReasons {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::KilledDueToTask => write!(f, "killed (due to task)"),
|
||||||
|
Self::ProcessEnded(exit_status) => {
|
||||||
|
if let Some(s) = exit_status.code() {
|
||||||
|
if s == 0 {
|
||||||
|
write!(f, "Stopped")
|
||||||
|
} else {
|
||||||
|
write!(f, "Stopped (Exited with status {s})!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(f, "Stopped!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::ProcessCouldNotBeSpawned(_e) => {
|
||||||
|
write!(f, "Couldn't spawn process (check your paths!)")
|
||||||
|
}
|
||||||
|
Self::ProcessCouldNotBeAwaited(_e) => write!(
|
||||||
|
f,
|
||||||
|
"Couldn't wait for process to end (check console/log for errors)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user