use std::collections::HashMap; use std::net::SocketAddr; use std::ops::DerefMut; use std::rc::Rc; use godot::classes::{Engine, IPanel, Panel, RichTextLabel, ScrollContainer, TextEdit}; use godot::prelude::*; use godot::tools::get_autoload_by_name; pub enum CliColor { Command, Info, Help, Error, } impl CliColor { pub fn as_str(&self) -> &str { match self { CliColor::Command => "#a2a2a2", CliColor::Info => "#fff", CliColor::Help => "#ffdc2e", CliColor::Error => "#cc3535", } } } pub const CLI_GLOBAL_NAME: &str = "CommandLinePanelGlobal"; #[derive(GodotClass)] #[class(base=Panel)] pub struct CommandLinePanel { #[export] pub input_field: Option>, #[export] pub text_field: Option>, #[export] pub scroll_container: Option>, scroll_next_update: bool, commands: HashMap>>, base: Base, } #[godot_api] impl IPanel for CommandLinePanel { fn init(base: Base) -> Self { let mut commands: HashMap>> = HashMap::new(); commands.insert("help".to_owned(), Rc::new(Box::new(HelpCommand))); commands.insert("host".to_string(), Rc::new(Box::new(HostCommand))); commands.insert("join".to_string(), Rc::new(Box::new(JoinCommand))); commands.insert( "disconnect".to_string(), Rc::new(Box::new(DisconnectCommand)), ); CommandLinePanel { input_field: None, text_field: None, scroll_container: None, scroll_next_update: false, commands, base, } } fn process(&mut self, _delta: f64) { if let Some(scroll) = &mut self.scroll_container && self.scroll_next_update { scroll.set_v_scroll(i32::MAX); self.scroll_next_update = false; } if let Some(input) = self.input_field.clone() { let text = input.get_text(); if text.contains("\n") { let parts = text.split("\n"); let first = parts.get(0).unwrap(); self.input_command(first.to_string()); } } } } #[godot_api] impl CommandLinePanel { #[signal] pub fn on_host(port: u16); #[signal] pub fn on_join(port: String); #[signal] pub fn on_disconnect(); pub fn singleton() -> Gd { get_autoload_by_name::(CLI_GLOBAL_NAME) } pub fn input_command(&mut self, command: String) { self.publish_message(format!("> {}", command), CliColor::Command); if let Some(input) = &mut self.input_field { input.clear(); } self.handle_command(&command); } pub fn publish_message(&mut self, message: String, color: CliColor) { if let Some(field) = &mut self.text_field { if !field.get_parsed_text().is_empty() { field.append_text("\n"); } field.append_text(&format!("[color={}]", color.as_str())); field.append_text(&message); field.append_text("[/color]"); self.scroll_next_update = true; } } fn handle_command(&mut self, text: &String) { let mut command_and_params = text.split(" ").into_iter(); let command = command_and_params.next().clone().unwrap_or(""); let params = command_and_params.collect::>(); if let Some(command) = self.commands.get(command) { command.clone().execute(self, ¶ms); } else { self.publish_message(format!("Unknown command: {}", command), CliColor::Info); } } } trait Command { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]); fn help(&self) -> &str; } #[derive(Clone)] struct HelpCommand; impl Command for HelpCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(command_name) = params.first() { if let Some(command) = cli.commands.get(*command_name) { cli.publish_message(command.help().to_string(), CliColor::Help); } else { cli.publish_message(format!("Unknown command: {}", command_name), CliColor::Help); } } else { let mut keys = cli.commands.keys().cloned().collect::>(); keys.sort(); for key in keys { cli.publish_message( format!("\t- {}: {}", key, cli.commands.get(&key).unwrap().help()), CliColor::Help, ); } } } fn help(&self) -> &str { "Shows this message" } } #[derive(Clone)] struct HostCommand; impl Command for HostCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(port_str) = params.first() { if let Ok(port) = u16::from_str_radix(*port_str, 10) { cli.signals().on_host().emit(port); } else { cli.publish_message(format!("Invalid port: {}", port_str), CliColor::Error); } } else { cli.publish_message(format!("Missing port"), CliColor::Error); cli.publish_message(format!("Usage: host [port]"), CliColor::Help); } } fn help(&self) -> &str { "Hosts a server at a given port" } } #[derive(Clone)] struct JoinCommand; impl Command for JoinCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(address_str) = params.first() { if let Ok(_) = address_str.parse::() { cli.signals().on_join().emit(address_str.to_string()); } else { cli.publish_message(format!("Invalid address: {}", address_str), CliColor::Error); } } else { cli.publish_message(format!("Missing address"), CliColor::Error); cli.publish_message(format!("Usage: join [address]:[port]"), CliColor::Help); } } fn help(&self) -> &str { "Joins server at a given address:port" } } #[derive(Clone)] struct DisconnectCommand; impl Command for DisconnectCommand { fn execute(&self, cli: &mut CommandLinePanel, _params: &[&str]) { cli.signals().on_disconnect().emit(); } fn help(&self) -> &str { "Disconnects from the server, or stops hosting the server" } }